Merge ~danilogondolfo/netplan/+git/ubuntu:jammy_0_107_1_sru_with_security_fixes into ~ubuntu-core-dev/netplan/+git/ubuntu:ubuntu-jammy

Proposed by Danilo Egea Gondolfo
Status: Merged
Merged at revision: 4a8e5ca2fcec7eb6a9dc9cfe534537d9759de694
Proposed branch: ~danilogondolfo/netplan/+git/ubuntu:jammy_0_107_1_sru_with_security_fixes
Merge into: ~ubuntu-core-dev/netplan/+git/ubuntu:ubuntu-jammy
Diff against target: 1961 lines (+1912/-0)
7 files modified
debian/changelog (+15/-0)
debian/netplan-generator.postinst (+15/-0)
debian/patches/lp2065738/0013-libnetplan-use-more-restrictive-file-permissions.patch (+435/-0)
debian/patches/lp2066258/0014-libnetplan-escape-control-characters.patch (+863/-0)
debian/patches/lp2066258/0015-backends-escape-file-paths.patch (+288/-0)
debian/patches/lp2066258/0016-backends-escape-semicolons-in-service-units.patch (+292/-0)
debian/patches/series (+4/-0)
Reviewer Review Type Date Requested Status
Lukas Märdian Approve
Ubuntu Core Development Team Pending
Review via email: mp+468522@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Lukas Märdian (slyon) wrote :

LGTM. The new security patches match the noble patches (ignoring patch fuzz).

- I updated d/changelog to not drop the previous 0.106.1-7ubuntu0.22.04.3 and 0.106.1-7ubuntu0.22.04.4 updates.
- I updated d/changelog to keep this as UNRELEASED for now, pending fixes utf-8 handling in g_strescape() (https://github.com/daniloegea/netplan/commits/utf8_strescape/)
- I wonder if we need to reconsider d/p/sru-compat/0013-Keep-old-file-permission-for-backwards-compatibility.patch regarding the recent security updates. Writing Netplan YAML (through "netplan set" CLI, that could contain secrets as 640 might not be wise... but OTOH we want to keep it backwards compatible. Maybe we should at least issue a big fat warning?

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/debian/changelog b/debian/changelog
2index e714607..a2dab85 100644
3--- a/debian/changelog
4+++ b/debian/changelog
5@@ -37,6 +37,21 @@ netplan.io (0.107.1-3ubuntu0.22.04.1) jammy; urgency=medium
6 - doc: create libnetplan apidoc structure (!423)
7 - inc: Start documenting public API (!423)
8 - doc: Update 'Netplan everywhere' for 23.10 (!418)
9+ SECURITY UPDATE: weak permissions on secret files, command injection
10+ - d/p/lp2065738/0014-libnetplan-use-more-restrictive-file-permissions.patch:
11+ Use more restrictive file permissions to prevent unprivileged users to
12+ read sensitive data from back end files (LP: 2065738, 1987842)
13+ - CVE-2022-4968
14+ - d/p/lp2066258/0015-libnetplan-escape-control-characters.patch:
15+ Escape control characters in the parser and double quotes in backend
16+ files.
17+ - d/p/lp2066258/0016-backends-escape-file-paths.patch:
18+ Escape special characters in file paths.
19+ - d/p/lp2066258/0017-backends-escape-semicolons-in-service-units.patch:
20+ Escape isolated semicolons in systemd service units. (LP: 2066258)
21+ - debian/netplan-generator.postinst: Add a postinst maintainer script to
22+ call the generator. It's needed so the file permissions fixes will be
23+ applied automatically.
24 Bug fixes:
25 - Fix FTBFS on Fedora and refresh RPM packaging (!323)
26 - parser: validate lacp-rate properly (LP: 1745648) (!324)
27diff --git a/debian/netplan-generator.postinst b/debian/netplan-generator.postinst
28new file mode 100644
29index 0000000..f805c24
30--- /dev/null
31+++ b/debian/netplan-generator.postinst
32@@ -0,0 +1,15 @@
33+#!/bin/sh
34+
35+set -e
36+
37+# Calling the generator after installation to mitigate CVE-2022-4968
38+# We avoid calling the generator if the system doesn't have networkd files to be fixed (LP: #2071333)
39+if [ "$1" = configure ]; then
40+ FILES=$(find /run/systemd/network/ -type f -regex ".*-netplan.*\.\(network\|netdev\)" 2>/dev/null || true)
41+ if [ -n "${FILES}" ]; then
42+ /usr/libexec/netplan/generate 2>/dev/null || echo "WARNING: Netplan could not re-generate network configuration. Please run 'netplan generate' to see details."
43+ fi
44+fi
45+
46+#DEBHELPER#
47+
48diff --git a/debian/patches/lp2065738/0013-libnetplan-use-more-restrictive-file-permissions.patch b/debian/patches/lp2065738/0013-libnetplan-use-more-restrictive-file-permissions.patch
49new file mode 100644
50index 0000000..9ebce53
51--- /dev/null
52+++ b/debian/patches/lp2065738/0013-libnetplan-use-more-restrictive-file-permissions.patch
53@@ -0,0 +1,435 @@
54+From: Danilo Egea Gondolfo <danilogondolfo@gmail.com>
55+Date: Wed, 22 May 2024 15:44:16 +0100
56+Subject: libnetplan: use more restrictive file permissions
57+
58+A new util.c:_netplan_g_string_free_to_file_with_permissions() was added
59+and accepts the owner, group and file mode as arguments. When these
60+properties can't be set, when the generator is called by a non-root user
61+for example, it will not hard-fail. This function is called by unit
62+tests where we can't set the owner to a privileged account for example.
63+
64+When generating backend files, use more restrictive permissions:
65+
66+networkd related files will be owned by root:systemd-network and have
67+mode 0640.
68+
69+service unit files will be owned by root:root and have mode 0640.
70+udevd files will be owned by root:root with mode 0640.
71+
72+wpa_supplicant and Network Manager files will continue with the existing
73+permissions.
74+
75+Autopkgtests will check if the permissions are set as expected when
76+calling the generator.
77+
78+This fix addresses CVE-2022-4968
79+---
80+ src/networkd.c | 36 ++++--------------
81+ src/networkd.h | 2 +
82+ src/nm.c | 4 +-
83+ src/openvswitch.c | 2 +-
84+ src/sriov.c | 2 +-
85+ src/util-internal.h | 3 ++
86+ src/util.c | 46 +++++++++++++++++++++++
87+ tests/generator/test_auth.py | 2 +-
88+ tests/generator/test_wifis.py | 2 +-
89+ tests/integration/base.py | 85 +++++++++++++++++++++++++++++++++++++++++++
90+ 10 files changed, 149 insertions(+), 35 deletions(-)
91+
92+diff --git a/src/networkd.c b/src/networkd.c
93+index 5554c99..1677381 100644
94+--- a/src/networkd.c
95++++ b/src/networkd.c
96+@@ -222,7 +222,6 @@ static void
97+ write_link_file(const NetplanNetDefinition* def, const char* rootdir, const char* path)
98+ {
99+ GString* s = NULL;
100+- mode_t orig_umask;
101+
102+ /* Don't write .link files for virtual devices; they use .netdev instead.
103+ * Don't write .link files for MODEM devices, as they aren't supported by networkd.
104+@@ -284,9 +283,7 @@ write_link_file(const NetplanNetDefinition* def, const char* rootdir, const char
105+ g_string_append_printf(s, "LargeReceiveOffload=%s\n",
106+ (def->large_receive_offload ? "true" : "false"));
107+
108+- orig_umask = umask(022);
109+- g_string_free_to_file(s, rootdir, path, ".link");
110+- umask(orig_umask);
111++ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, ".link", "root", "root", 0640);
112+ }
113+
114+ static gboolean
115+@@ -304,7 +301,7 @@ write_regdom(const NetplanNetDefinition* def, const char* rootdir, GError** erro
116+ g_string_append(s, "\n[Service]\nType=oneshot\n");
117+ g_string_append_printf(s, "ExecStart="SBINDIR"/iw reg set %s\n", def->regulatory_domain);
118+
119+- g_string_free_to_file(s, rootdir, path, NULL);
120++ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
121+ safe_mkdir_p_dir(link);
122+ if (symlink(path, link) < 0 && errno != EEXIST) {
123+ // LCOV_EXCL_START
124+@@ -484,7 +481,6 @@ static void
125+ write_netdev_file(const NetplanNetDefinition* def, const char* rootdir, const char* path)
126+ {
127+ GString* s = NULL;
128+- mode_t orig_umask;
129+
130+ g_assert(def->type >= NETPLAN_DEF_TYPE_VIRTUAL);
131+
132+@@ -580,11 +576,7 @@ write_netdev_file(const NetplanNetDefinition* def, const char* rootdir, const ch
133+ default: g_assert_not_reached(); // LCOV_EXCL_LINE
134+ }
135+
136+- /* these do not contain secrets and need to be readable by
137+- * systemd-networkd - LP: #1736965 */
138+- orig_umask = umask(022);
139+- g_string_free_to_file(s, rootdir, path, ".netdev");
140+- umask(orig_umask);
141++ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, ".netdev", "root", NETWORKD_GROUP, 0640);
142+ }
143+
144+ static void
145+@@ -728,7 +720,6 @@ netplan_netdef_write_network_file(
146+ g_autoptr(GString) network = NULL;
147+ g_autoptr(GString) link = NULL;
148+ GString* s = NULL;
149+- mode_t orig_umask;
150+ gboolean is_optional = def->optional;
151+
152+ SET_OPT_OUT_PTR(has_been_written, FALSE);
153+@@ -979,11 +970,7 @@ netplan_netdef_write_network_file(
154+ if (network->len > 0)
155+ g_string_append_printf(s, "\n[Network]\n%s", network->str);
156+
157+- /* these do not contain secrets and need to be readable by
158+- * systemd-networkd - LP: #1736965 */
159+- orig_umask = umask(022);
160+- g_string_free_to_file(s, rootdir, path, ".network");
161+- umask(orig_umask);
162++ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, ".network", "root", NETWORKD_GROUP, 0640);
163+ }
164+
165+ SET_OPT_OUT_PTR(has_been_written, TRUE);
166+@@ -995,7 +982,6 @@ write_rules_file(const NetplanNetDefinition* def, const char* rootdir)
167+ {
168+ GString* s = NULL;
169+ g_autofree char* path = g_strjoin(NULL, "run/udev/rules.d/99-netplan-", def->id, ".rules", NULL);
170+- mode_t orig_umask;
171+
172+ /* do we need to write a .rules file?
173+ * It's only required for reliably setting the name of a physical device
174+@@ -1029,9 +1015,7 @@ write_rules_file(const NetplanNetDefinition* def, const char* rootdir)
175+
176+ g_string_append_printf(s, "NAME=\"%s\"\n", def->set_name);
177+
178+- orig_umask = umask(022);
179+- g_string_free_to_file(s, rootdir, path, NULL);
180+- umask(orig_umask);
181++ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
182+ }
183+
184+ static gboolean
185+@@ -1180,7 +1164,6 @@ static void
186+ write_wpa_unit(const NetplanNetDefinition* def, const char* rootdir)
187+ {
188+ g_autofree gchar *stdouth = NULL;
189+- mode_t orig_umask;
190+
191+ stdouth = systemd_escape(def->id);
192+
193+@@ -1199,9 +1182,7 @@ write_wpa_unit(const NetplanNetDefinition* def, const char* rootdir)
194+ } else {
195+ g_string_append(s, " -Dnl80211,wext\n");
196+ }
197+- orig_umask = umask(022);
198+- g_string_free_to_file(s, rootdir, path, NULL);
199+- umask(orig_umask);
200++ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
201+ }
202+
203+ static gboolean
204+@@ -1210,7 +1191,6 @@ write_wpa_conf(const NetplanNetDefinition* def, const char* rootdir, GError** er
205+ GHashTableIter iter;
206+ GString* s = g_string_new("ctrl_interface=/run/wpa_supplicant\n\n");
207+ g_autofree char* path = g_strjoin(NULL, "run/netplan/wpa-", def->id, ".conf", NULL);
208+- mode_t orig_umask;
209+
210+ g_debug("%s: Creating wpa_supplicant configuration file %s", def->id, path);
211+ if (def->type == NETPLAN_DEF_TYPE_WIFI) {
212+@@ -1299,9 +1279,7 @@ write_wpa_conf(const NetplanNetDefinition* def, const char* rootdir, GError** er
213+ }
214+
215+ /* use tight permissions as this contains secrets */
216+- orig_umask = umask(077);
217+- g_string_free_to_file(s, rootdir, path, NULL);
218+- umask(orig_umask);
219++ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0600);
220+ return TRUE;
221+ }
222+
223+diff --git a/src/networkd.h b/src/networkd.h
224+index a7092b2..0214e43 100644
225+--- a/src/networkd.h
226++++ b/src/networkd.h
227+@@ -20,6 +20,8 @@
228+ #include "netplan.h"
229+ #include <glib.h>
230+
231++#define NETWORKD_GROUP "systemd-network"
232++
233+ NETPLAN_INTERNAL gboolean
234+ netplan_netdef_write_networkd(
235+ const NetplanState* np_state,
236+diff --git a/src/nm.c b/src/nm.c
237+index 4d6f1fe..d5dad98 100644
238+--- a/src/nm.c
239++++ b/src/nm.c
240+@@ -1149,13 +1149,13 @@ netplan_state_finish_nm_write(
241+
242+ /* write generated NetworkManager drop-in config */
243+ if (nm_conf->len > 0)
244+- g_string_free_to_file(nm_conf, rootdir, "run/NetworkManager/conf.d/netplan.conf", NULL);
245++ _netplan_g_string_free_to_file_with_permissions(nm_conf, rootdir, "run/NetworkManager/conf.d/netplan.conf", NULL, "root", "root", 0640);
246+ else
247+ g_string_free(nm_conf, TRUE);
248+
249+ /* write generated udev rules */
250+ if (udev_rules->len > 0)
251+- g_string_free_to_file(udev_rules, rootdir, "run/udev/rules.d/90-netplan.rules", NULL);
252++ _netplan_g_string_free_to_file_with_permissions(udev_rules, rootdir, "run/udev/rules.d/90-netplan.rules", NULL, "root", "root", 0640);
253+ else
254+ g_string_free(udev_rules, TRUE);
255+
256+diff --git a/src/openvswitch.c b/src/openvswitch.c
257+index d4af861..276762e 100644
258+--- a/src/openvswitch.c
259++++ b/src/openvswitch.c
260+@@ -62,7 +62,7 @@ write_ovs_systemd_unit(const char* id, const GString* cmds, const char* rootdir,
261+ g_string_append(s, "\n[Service]\nType=oneshot\nTimeoutStartSec=10s\n");
262+ g_string_append(s, cmds->str);
263+
264+- g_string_free_to_file(s, rootdir, path, NULL);
265++ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
266+
267+ safe_mkdir_p_dir(link);
268+ if (symlink(path, link) < 0 && errno != EEXIST) {
269+diff --git a/src/sriov.c b/src/sriov.c
270+index f8117f7..c3cd80d 100644
271+--- a/src/sriov.c
272++++ b/src/sriov.c
273+@@ -53,7 +53,7 @@ write_sriov_rebind_systemd_unit(GHashTable* pfs, const char* rootdir, GError** e
274+ g_string_truncate(interfaces, interfaces->len-1); /* cut trailing whitespace */
275+ g_string_append_printf(s, "ExecStart=" SBINDIR "/netplan rebind %s\n", interfaces->str);
276+
277+- g_string_free_to_file(s, rootdir, path, NULL);
278++ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
279+ g_string_free(interfaces, TRUE);
280+
281+ safe_mkdir_p_dir(link);
282+diff --git a/src/util-internal.h b/src/util-internal.h
283+index fe41c77..b30745b 100644
284+--- a/src/util-internal.h
285++++ b/src/util-internal.h
286+@@ -40,6 +40,9 @@ safe_mkdir_p_dir(const char* file_path);
287+ NETPLAN_INTERNAL void
288+ g_string_free_to_file(GString* s, const char* rootdir, const char* path, const char* suffix);
289+
290++void
291++_netplan_g_string_free_to_file_with_permissions(GString* s, const char* rootdir, const char* path, const char* suffix, const char* owner, const char* group, mode_t mode);
292++
293+ NETPLAN_INTERNAL void
294+ unlink_glob(const char* rootdir, const char* _glob);
295+
296+diff --git a/src/util.c b/src/util.c
297+index cbd5ac2..edecdab 100644
298+--- a/src/util.c
299++++ b/src/util.c
300+@@ -22,6 +22,9 @@
301+ #include <errno.h>
302+ #include <string.h>
303+ #include <sys/mman.h>
304++#include <sys/types.h>
305++#include <pwd.h>
306++#include <grp.h>
307+
308+ #include <glib.h>
309+ #include <glib/gprintf.h>
310+@@ -87,6 +90,49 @@ void g_string_free_to_file(GString* s, const char* rootdir, const char* path, co
311+ }
312+ }
313+
314++void _netplan_g_string_free_to_file_with_permissions(GString* s, const char* rootdir, const char* path, const char* suffix, const char* owner, const char* group, mode_t mode)
315++{
316++ g_autofree char* full_path = NULL;
317++ g_autofree char* path_suffix = NULL;
318++ g_autofree char* contents = g_string_free(s, FALSE);
319++ GError* error = NULL;
320++ struct passwd* pw = NULL;
321++ struct group* gr = NULL;
322++ int ret = 0;
323++
324++ path_suffix = g_strjoin(NULL, path, suffix, NULL);
325++ full_path = g_build_path(G_DIR_SEPARATOR_S, rootdir ?: G_DIR_SEPARATOR_S, path_suffix, NULL);
326++ safe_mkdir_p_dir(full_path);
327++ if (!g_file_set_contents_full(full_path, contents, -1, G_FILE_SET_CONTENTS_CONSISTENT | G_FILE_SET_CONTENTS_ONLY_EXISTING, mode, &error)) {
328++ /* the mkdir() just succeeded, there is no sensible
329++ * method to test this without root privileges, bind mounts, and
330++ * simulating ENOSPC */
331++ // LCOV_EXCL_START
332++ g_fprintf(stderr, "ERROR: cannot create file %s: %s\n", path, error->message);
333++ exit(1);
334++ // LCOV_EXCL_STOP
335++ }
336++
337++ /* Here we take the owner and group names and look up for their IDs in the passwd and group files.
338++ * It's OK to fail to set the owners and mode as this code will be called from unit tests.
339++ * The autopkgtests will check if the owner/group and mode are correctly set.
340++ */
341++ pw = getpwnam(owner);
342++ if (!pw) {
343++ g_debug("Failed to determine the UID of user %s: %s", owner, strerror(errno)); // LCOV_EXCL_LINE
344++ }
345++ gr = getgrnam(group);
346++ if (!gr) {
347++ g_debug("Failed to determine the GID of group %s: %s", group, strerror(errno)); // LCOV_EXCL_LINE
348++ }
349++ if (pw && gr) {
350++ ret = chown(full_path, pw->pw_uid, gr->gr_gid);
351++ if (ret != 0) {
352++ g_debug("Failed to set owner and group for file %s: %s", full_path, strerror(errno));
353++ }
354++ }
355++}
356++
357+ /**
358+ * Remove all files matching given glob.
359+ */
360+diff --git a/tests/generator/test_auth.py b/tests/generator/test_auth.py
361+index de23adb..d3d886c 100644
362+--- a/tests/generator/test_auth.py
363++++ b/tests/generator/test_auth.py
364+@@ -226,7 +226,7 @@ network={
365+
366+ with open(os.path.join(self.workdir.name, 'run/systemd/system/netplan-wpa-eth0.service')) as f:
367+ self.assertEqual(f.read(), SD_WPA % {'iface': 'eth0', 'drivers': 'wired'})
368+- self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o644)
369++ self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o640)
370+ self.assertTrue(os.path.islink(os.path.join(
371+ self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-eth0.service')))
372+
373+diff --git a/tests/generator/test_wifis.py b/tests/generator/test_wifis.py
374+index b875172..610782a 100644
375+--- a/tests/generator/test_wifis.py
376++++ b/tests/generator/test_wifis.py
377+@@ -140,7 +140,7 @@ network={
378+ self.workdir.name, 'run/systemd/system/netplan-wpa-wl0.service')))
379+ with open(os.path.join(self.workdir.name, 'run/systemd/system/netplan-wpa-wl0.service')) as f:
380+ self.assertEqual(f.read(), SD_WPA % {'iface': 'wl0', 'drivers': 'nl80211,wext'})
381+- self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o644)
382++ self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o640)
383+ self.assertTrue(os.path.islink(os.path.join(
384+ self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-wl0.service')))
385+
386+diff --git a/tests/integration/base.py b/tests/integration/base.py
387+index 81e8420..948b1c5 100644
388+--- a/tests/integration/base.py
389++++ b/tests/integration/base.py
390+@@ -32,6 +32,8 @@ import shutil
391+ import gi
392+ import glob
393+ import json
394++import pwd
395++import grp
396+
397+ # make sure we point to libnetplan properly.
398+ os.environ.update({'LD_LIBRARY_PATH': '.:{}'.format(os.environ.get('LD_LIBRARY_PATH'))})
399+@@ -367,6 +369,89 @@ class IntegrationTestsBase(unittest.TestCase):
400+ if state:
401+ self.wait_output(['ip', 'addr', 'show', iface], state, 30)
402+
403++ # Assert file permissions
404++ self.assert_file_permissions()
405++
406++ def assert_file_permissions(self):
407++ """ Check if the generated files have the expected permissions """
408++
409++ nd_expected_mode = 0o100640
410++ nd_expected_owner = 'root'
411++ nd_expected_group = 'systemd-network'
412++
413++ sd_expected_mode = 0o100640
414++ sd_expected_owner = 'root'
415++ sd_expected_group = 'root'
416++
417++ udev_expected_mode = 0o100640
418++ udev_expected_owner = 'root'
419++ udev_expected_group = 'root'
420++
421++ nm_expected_mode = 0o100600
422++ nm_expected_owner = 'root'
423++ nm_expected_group = 'root'
424++
425++ wpa_expected_mode = 0o100600
426++ wpa_expected_owner = 'root'
427++ wpa_expected_group = 'root'
428++
429++ # Check systemd-networkd files
430++ base_path = '/run/systemd/network'
431++ files = glob.glob(f'{base_path}/*.network') + glob.glob(f'{base_path}/*.netdev')
432++ for file in files:
433++ res = os.stat(file)
434++ user = pwd.getpwuid(res.st_uid)
435++ group = grp.getgrgid(res.st_gid)
436++ self.assertEqual(res.st_mode, nd_expected_mode, f'file {file}')
437++ self.assertEqual(user.pw_name, nd_expected_owner, f'file {file}')
438++ self.assertEqual(group.gr_name, nd_expected_group, f'file {file}')
439++
440++ # Check Network Manager files
441++ base_path = '/run/NetworkManager/system-connections'
442++ files = glob.glob(f'{base_path}/*.nmconnection')
443++ for file in files:
444++ res = os.stat(file)
445++ user = pwd.getpwuid(res.st_uid)
446++ group = grp.getgrgid(res.st_gid)
447++ self.assertEqual(res.st_mode, nm_expected_mode, f'file {file}')
448++ self.assertEqual(user.pw_name, nm_expected_owner, f'file {file}')
449++ self.assertEqual(group.gr_name, nm_expected_group, f'file {file}')
450++
451++ # Check wpa_supplicant configuration files
452++ base_path = '/run/netplan'
453++ files = glob.glob(f'{base_path}/wpa-*.conf')
454++ for file in files:
455++ res = os.stat(file)
456++ user = pwd.getpwuid(res.st_uid)
457++ group = grp.getgrgid(res.st_gid)
458++ self.assertEqual(res.st_mode, wpa_expected_mode, f'file {file}')
459++ self.assertEqual(user.pw_name, wpa_expected_owner, f'file {file}')
460++ self.assertEqual(group.gr_name, wpa_expected_group, f'file {file}')
461++
462++ # Check systemd service unit files
463++ base_path = '/run/systemd/system/'
464++ files = glob.glob(f'{base_path}/netplan-*.service')
465++ files += glob.glob(f'{base_path}/systemd-networkd-wait-online.service.d/*.conf')
466++ for file in files:
467++ res = os.stat(file)
468++ user = pwd.getpwuid(res.st_uid)
469++ group = grp.getgrgid(res.st_gid)
470++ self.assertEqual(res.st_mode, sd_expected_mode, f'file {file}')
471++ self.assertEqual(user.pw_name, sd_expected_owner, f'file {file}')
472++ self.assertEqual(group.gr_name, sd_expected_group, f'file {file}')
473++
474++ # Check systemd-udevd files
475++ udev_path = '/run/udev/rules.d'
476++ link_path = '/run/systemd/network'
477++ files = glob.glob(f'{udev_path}/*-netplan*.rules') + glob.glob(f'{link_path}/*.link')
478++ for file in files:
479++ res = os.stat(file)
480++ user = pwd.getpwuid(res.st_uid)
481++ group = grp.getgrgid(res.st_gid)
482++ self.assertEqual(res.st_mode, udev_expected_mode, f'file {file}')
483++ self.assertEqual(user.pw_name, udev_expected_owner, f'file {file}')
484++ self.assertEqual(group.gr_name, udev_expected_group, f'file {file}')
485++
486+ def state(self, iface, state):
487+ '''Tell generate_and_settle() to wait for a specific state'''
488+ return iface + '/' + state
489diff --git a/debian/patches/lp2066258/0014-libnetplan-escape-control-characters.patch b/debian/patches/lp2066258/0014-libnetplan-escape-control-characters.patch
490new file mode 100644
491index 0000000..d21ff90
492--- /dev/null
493+++ b/debian/patches/lp2066258/0014-libnetplan-escape-control-characters.patch
494@@ -0,0 +1,863 @@
495+From: Danilo Egea Gondolfo <danilogondolfo@gmail.com>
496+Date: Wed, 29 May 2024 14:50:55 +0100
497+Subject: libnetplan: escape control characters
498+
499+Control characters are escaped in the parser using glib's g_strescape.
500+Quotes and backslashes were added to the list of exception.
501+
502+In places where double quotes are not escaped, such as netdef IDs as it
503+is allowed as interface names, they are escaped as needed when
504+generating back end configuration.
505+
506+To support escaping in wpa_supplicant configuration, the syntax for
507+setting the SSID was changed to 'ssid=P"string here"'. With that,
508+escaping is support in a printf-style.
509+---
510+ src/networkd.c | 32 ++++++++++-----
511+ src/nm.c | 21 ++++++----
512+ src/parse.c | 92 +++++++++++++++++++++++++++---------------
513+ src/util-internal.h | 3 ++
514+ src/util.c | 11 +++++
515+ tests/generator/test_auth.py | 20 ++++-----
516+ tests/generator/test_common.py | 42 +++++++++++++++++--
517+ tests/generator/test_wifis.py | 78 ++++++++++++++++++++++++++---------
518+ 8 files changed, 216 insertions(+), 83 deletions(-)
519+
520+diff --git a/src/networkd.c b/src/networkd.c
521+index 1677381..ac54112 100644
522+--- a/src/networkd.c
523++++ b/src/networkd.c
524+@@ -1005,7 +1005,8 @@ write_rules_file(const NetplanNetDefinition* def, const char* rootdir)
525+ g_string_append(s, "SUBSYSTEM==\"net\", ACTION==\"add\", ");
526+
527+ if (def->match.driver) {
528+- g_string_append_printf(s,"DRIVERS==\"%s\", ", def->match.driver);
529++ g_autofree char* driver = _netplan_scrub_string(def->match.driver);
530++ g_string_append_printf(s,"DRIVERS==\"%s\", ", driver);
531+ } else {
532+ g_string_append(s, "DRIVERS==\"?*\", ");
533+ }
534+@@ -1013,7 +1014,8 @@ write_rules_file(const NetplanNetDefinition* def, const char* rootdir)
535+ if (def->match.mac)
536+ g_string_append_printf(s, "ATTR{address}==\"%s\", ", def->match.mac);
537+
538+- g_string_append_printf(s, "NAME=\"%s\"\n", def->set_name);
539++ g_autofree char* set_name = _netplan_scrub_string(def->set_name);
540++ g_string_append_printf(s, "NAME=\"%s\"\n", set_name);
541+
542+ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
543+ }
544+@@ -1101,10 +1103,12 @@ append_wpa_auth_conf(GString* s, const NetplanAuthenticationSettings* auth, cons
545+ }
546+
547+ if (auth->identity) {
548+- g_string_append_printf(s, " identity=\"%s\"\n", auth->identity);
549++ g_autofree char* identity = _netplan_scrub_string(auth->identity);
550++ g_string_append_printf(s, " identity=\"%s\"\n", identity);
551+ }
552+ if (auth->anonymous_identity) {
553+- g_string_append_printf(s, " anonymous_identity=\"%s\"\n", auth->anonymous_identity);
554++ g_autofree char* anonymous_identity = _netplan_scrub_string(auth->anonymous_identity);
555++ g_string_append_printf(s, " anonymous_identity=\"%s\"\n", anonymous_identity);
556+ }
557+
558+ char* psk = NULL;
559+@@ -1142,19 +1146,23 @@ append_wpa_auth_conf(GString* s, const NetplanAuthenticationSettings* auth, cons
560+ }
561+ }
562+ if (auth->ca_certificate) {
563+- g_string_append_printf(s, " ca_cert=\"%s\"\n", auth->ca_certificate);
564++ g_autofree char* ca_certificate = _netplan_scrub_string(auth->ca_certificate);
565++ g_string_append_printf(s, " ca_cert=\"%s\"\n", ca_certificate);
566+ }
567+ if (auth->client_certificate) {
568+- g_string_append_printf(s, " client_cert=\"%s\"\n", auth->client_certificate);
569++ g_autofree char* client_certificate = _netplan_scrub_string(auth->client_certificate);
570++ g_string_append_printf(s, " client_cert=\"%s\"\n", client_certificate);
571+ }
572+ if (auth->client_key) {
573+- g_string_append_printf(s, " private_key=\"%s\"\n", auth->client_key);
574++ g_autofree char* client_key = _netplan_scrub_string(auth->client_key);
575++ g_string_append_printf(s, " private_key=\"%s\"\n", client_key);
576+ }
577+ if (auth->client_key_password) {
578+ g_string_append_printf(s, " private_key_passwd=\"%s\"\n", auth->client_key_password);
579+ }
580+ if (auth->phase2_auth) {
581+- g_string_append_printf(s, " phase2=\"auth=%s\"\n", auth->phase2_auth);
582++ g_autofree char* phase2_auth = _netplan_scrub_string(auth->phase2_auth);
583++ g_string_append_printf(s, " phase2=\"auth=%s\"\n", phase2_auth);
584+ }
585+ return TRUE;
586+ }
587+@@ -1203,14 +1211,16 @@ write_wpa_conf(const NetplanNetDefinition* def, const char* rootdir, GError** er
588+ }
589+ /* available as of wpa_supplicant version 0.6.7 */
590+ if (def->regulatory_domain) {
591+- g_string_append_printf(s, "country=%s\n", def->regulatory_domain);
592++ g_autofree char* regdom = _netplan_scrub_string(def->regulatory_domain);
593++ g_string_append_printf(s, "country=%s\n", regdom);
594+ }
595+ NetplanWifiAccessPoint* ap;
596+ g_hash_table_iter_init(&iter, def->access_points);
597+ while (g_hash_table_iter_next(&iter, NULL, (gpointer) &ap)) {
598+ gchar* freq_config_str = ap->mode == NETPLAN_WIFI_MODE_ADHOC ? "frequency" : "freq_list";
599++ g_autofree char* ssid = _netplan_scrub_string(ap->ssid);
600+
601+- g_string_append_printf(s, "network={\n ssid=\"%s\"\n", ap->ssid);
602++ g_string_append_printf(s, "network={\n ssid=P\"%s\"\n", ssid);
603+ if (ap->bssid) {
604+ g_string_append_printf(s, " bssid=%s\n", ap->bssid);
605+ }
606+@@ -1257,7 +1267,7 @@ write_wpa_conf(const NetplanNetDefinition* def, const char* rootdir, GError** er
607+
608+ /* wifi auth trumps netdef auth */
609+ if (ap->has_auth) {
610+- if (!append_wpa_auth_conf(s, &ap->auth, ap->ssid, error)) {
611++ if (!append_wpa_auth_conf(s, &ap->auth, ssid, error)) {
612+ g_string_free(s, TRUE);
613+ return FALSE;
614+ }
615+diff --git a/src/nm.c b/src/nm.c
616+index d5dad98..a605c38 100644
617+--- a/src/nm.c
618++++ b/src/nm.c
619+@@ -1081,28 +1081,30 @@ netplan_state_finish_nm_write(
620+ GString *tmp = NULL;
621+ guint unmanaged = nd->backend == NETPLAN_BACKEND_NM ? 0 : 1;
622+
623++ g_autofree char* netdef_id = _netplan_scrub_string(nd->id);
624+ /* Special case: manage or ignore any device of given type on empty "match: {}" stanza */
625+ if (nd->has_match && !nd->match.driver && !nd->match.mac && !nd->match.original_name) {
626+ nm_type = type_str(nd);
627+ g_assert(nm_type);
628+ g_string_append_printf(nm_conf, "[device-netplan.%s.%s]\nmatch-device=type:%s\n"
629+ "managed=%d\n\n", netplan_def_type_name(nd->type),
630+- nd->id, nm_type, !unmanaged);
631++ netdef_id, nm_type, !unmanaged);
632+ }
633+ /* Normal case: manage or ignore devices by specific udev rules */
634+ else {
635+ const gchar *prefix = "SUBSYSTEM==\"net\", ACTION==\"add|change|move\",";
636+ const gchar *suffix = nd->backend == NETPLAN_BACKEND_NM ? " ENV{NM_UNMANAGED}=\"0\"\n" : " ENV{NM_UNMANAGED}=\"1\"\n";
637+ g_string_append_printf(udev_rules, "# netplan: network.%s.%s (on NetworkManager %s)\n",
638+- netplan_def_type_name(nd->type), nd->id,
639++ netplan_def_type_name(nd->type), netdef_id,
640+ unmanaged ? "deny-list" : "allow-list");
641+ /* Match by explicit interface name, if possible */
642+ if (nd->set_name) {
643+ // simple case: explicit new interface name
644+- g_string_append_printf(udev_rules, "%s ENV{ID_NET_NAME}==\"%s\",%s", prefix, nd->set_name, suffix);
645++ g_autofree char* set_name = _netplan_scrub_string(nd->set_name);
646++ g_string_append_printf(udev_rules, "%s ENV{ID_NET_NAME}==\"%s\",%s", prefix, set_name, suffix);
647+ } else if (!nd->has_match) {
648+ // simple case: explicit netplan ID is interface name
649+- g_string_append_printf(udev_rules, "%s ENV{ID_NET_NAME}==\"%s\",%s", prefix, nd->id, suffix);
650++ g_string_append_printf(udev_rules, "%s ENV{ID_NET_NAME}==\"%s\",%s", prefix, netdef_id, suffix);
651+ }
652+ /* Also, match by explicit (new) MAC, if available */
653+ if (nd->set_mac) {
654+@@ -1118,9 +1120,10 @@ netplan_state_finish_nm_write(
655+ // match on original name glob
656+ // TODO: maybe support matching on multiple name globs in the future (like drivers)
657+ g_string_append(udev_rules, prefix);
658+- if (nd->match.original_name)
659+- g_string_append_printf(udev_rules, " ENV{ID_NET_NAME}==\"%s\",", nd->match.original_name);
660+-
661++ if (nd->match.original_name) {
662++ g_autofree char* original_name = _netplan_scrub_string(nd->match.original_name);
663++ g_string_append_printf(udev_rules, " ENV{ID_NET_NAME}==\"%s\",", original_name);
664++ }
665+ // match on (explicit) MAC address. Yes this would be unique on its own, but we
666+ // keep it within the "full match" to make the logic more comprehensible.
667+ if (nd->match.mac) {
668+@@ -1138,7 +1141,9 @@ netplan_state_finish_nm_write(
669+ g_strfreev(split);
670+ } else
671+ drivers = g_strdup(nd->match.driver);
672+- g_string_append_printf(udev_rules, " ENV{ID_NET_DRIVER}==\"%s\",", drivers);
673++
674++ g_autofree char* escaped_drivers = _netplan_scrub_string(drivers);
675++ g_string_append_printf(udev_rules, " ENV{ID_NET_DRIVER}==\"%s\",", escaped_drivers);
676+ g_free(drivers);
677+ }
678+ g_string_append(udev_rules, suffix);
679+diff --git a/src/parse.c b/src/parse.c
680+index 143f908..9cb941a 100644
681+--- a/src/parse.c
682++++ b/src/parse.c
683+@@ -59,6 +59,15 @@ extern NetplanState global_state;
684+
685+ NetplanParser global_parser = {0};
686+
687++/*
688++ * We use g_strescape to escape control characters from the input.
689++ * Besides control characters, g_strescape will also escape double quotes and backslashes.
690++ * Quotes are escaped at configuration generation time as needed, as they might be part of passwords for example.
691++ * Escaping backslashes in the parser affects "netplan set" as it will always escape \'s from
692++ * the input and update YAMLs with all the \'s escaped again.
693++*/
694++static char* STRESCAPE_EXCEPTIONS = "\"\\";
695++
696+ static gboolean
697+ insert_kv_into_hash(void *key, void *value, void *hash);
698+
699+@@ -365,7 +374,7 @@ handle_generic_str(NetplanParser* npp, yaml_node_t* node, void* entryptr, const
700+ guint offset = GPOINTER_TO_UINT(data);
701+ char** dest = (char**) ((void*) entryptr + offset);
702+ g_free(*dest);
703+- *dest = g_strdup(scalar(node));
704++ *dest = g_strescape(scalar(node), STRESCAPE_EXCEPTIONS);
705+ mark_data_as_dirty(npp, dest);
706+ return TRUE;
707+ }
708+@@ -480,22 +489,25 @@ handle_generic_map(NetplanParser *npp, yaml_node_t* node, const char* key_prefix
709+ assert_type(npp, key, YAML_SCALAR_NODE);
710+ assert_type(npp, value, YAML_SCALAR_NODE);
711+
712++ g_autofree char* escaped_key = g_strescape(scalar(key), STRESCAPE_EXCEPTIONS);
713++ g_autofree char* escaped_value = g_strescape(scalar(value), STRESCAPE_EXCEPTIONS);
714++
715+ if (key_prefix && npp->null_fields) {
716+ g_autofree char* full_key = NULL;
717+- full_key = g_strdup_printf("%s\t%s", key_prefix, key->data.scalar.value);
718++ full_key = g_strdup_printf("%s\t%s", key_prefix, escaped_key);
719+ if (g_hash_table_contains(npp->null_fields, full_key))
720+ continue;
721+ }
722+
723+ char* stored_value = NULL;
724+- if (g_hash_table_lookup_extended(*map, scalar(key), NULL, (void**)&stored_value)) {
725++ if (g_hash_table_lookup_extended(*map, escaped_key, NULL, (void**)&stored_value)) {
726+ /* We can safely skip this if it is the exact key/value match
727+ * (probably caused by multi-pass processing) */
728+- if (g_strcmp0(stored_value, scalar(value)) == 0)
729++ if (g_strcmp0(stored_value, escaped_value) == 0)
730+ continue;
731+- return yaml_error(npp, node, error, "duplicate map entry '%s'", scalar(key));
732++ return yaml_error(npp, node, error, "duplicate map entry '%s'", escaped_key);
733+ } else
734+- g_hash_table_insert(*map, g_strdup(scalar(key)), g_strdup(scalar(value)));
735++ g_hash_table_insert(*map, g_strdup(escaped_key), g_strdup(escaped_value));
736+ }
737+ mark_data_as_dirty(npp, map);
738+
739+@@ -518,20 +530,26 @@ handle_generic_datalist(NetplanParser *npp, yaml_node_t* node, const char* key_p
740+ for (yaml_node_pair_t* entry = node->data.mapping.pairs.start; entry < node->data.mapping.pairs.top; entry++) {
741+ yaml_node_t* key, *value;
742+ g_autofree char* full_key = NULL;
743++ g_autofree char* escaped_key = NULL;
744++ g_autofree char* escaped_value = NULL;
745+
746+ key = yaml_document_get_node(&npp->doc, entry->key);
747+ value = yaml_document_get_node(&npp->doc, entry->value);
748+
749+ assert_type(npp, key, YAML_SCALAR_NODE);
750+ assert_type(npp, value, YAML_SCALAR_NODE);
751++
752++ escaped_key = g_strescape(scalar(key), STRESCAPE_EXCEPTIONS);
753++ escaped_value = g_strescape(scalar(value), STRESCAPE_EXCEPTIONS);
754++
755+ if (npp->null_fields && key_prefix) {
756+- full_key = g_strdup_printf("%s\t%s", key_prefix, scalar(key));
757++ full_key = g_strdup_printf("%s\t%s", key_prefix, escaped_key);
758+ if (g_hash_table_contains(npp->null_fields, full_key))
759+ continue;
760+ }
761+
762+- g_datalist_id_set_data_full(list, g_quark_from_string(scalar(key)),
763+- g_strdup(scalar(value)), g_free);
764++ g_datalist_id_set_data_full(list, g_quark_from_string(escaped_key),
765++ g_strdup(escaped_value), g_free);
766+ }
767+ mark_data_as_dirty(npp, list);
768+
769+@@ -864,13 +882,14 @@ handle_match_driver(NetplanParser* npp, yaml_node_t* node, __unused const char*
770+ for (yaml_node_item_t *iter = node->data.sequence.items.start; iter < node->data.sequence.items.top; iter++) {
771+ elem = yaml_document_get_node(&npp->doc, *iter);
772+ assert_type(npp, elem, YAML_SCALAR_NODE);
773+- if (g_strrstr(scalar(elem), " "))
774++ g_autofree char* escaped_elem = g_strescape(scalar(elem), STRESCAPE_EXCEPTIONS);
775++ if (g_strrstr(escaped_elem, " "))
776+ return yaml_error(npp, node, error, "A 'driver' glob cannot contain whitespace");
777+
778+ if (!sequence)
779+- sequence = g_string_new(scalar(elem));
780++ sequence = g_string_new(escaped_elem);
781+ else
782+- g_string_append_printf(sequence, "\t%s", scalar(elem)); /* tab separated */
783++ g_string_append_printf(sequence, "\t%s", escaped_elem); /* tab separated */
784+ }
785+
786+ if (!sequence)
787+@@ -902,7 +921,7 @@ handle_auth_str(NetplanParser* npp, yaml_node_t* node, const void* data, __unuse
788+ guint offset = GPOINTER_TO_UINT(data);
789+ char** dest = (char**) ((void*) npp->current.auth + offset);
790+ g_free(*dest);
791+- *dest = g_strdup(scalar(node));
792++ *dest = g_strescape(scalar(node), STRESCAPE_EXCEPTIONS);
793+ mark_data_as_dirty(npp, dest);
794+ return TRUE;
795+ }
796+@@ -1050,7 +1069,7 @@ handle_access_point_password(NetplanParser* npp, yaml_node_t* node, __unused con
797+
798+ access_point->auth.pmf_mode = NETPLAN_AUTH_PMF_MODE_OPTIONAL;
799+ g_free(access_point->auth.psk);
800+- access_point->auth.psk = g_strdup(scalar(node));
801++ access_point->auth.psk = g_strescape(scalar(node), STRESCAPE_EXCEPTIONS);
802+ return TRUE;
803+ }
804+
805+@@ -1434,6 +1453,7 @@ handle_wifi_access_points(NetplanParser* npp, yaml_node_t* node, const char* key
806+ for (yaml_node_pair_t* entry = node->data.mapping.pairs.start; entry < node->data.mapping.pairs.top; entry++) {
807+ NetplanWifiAccessPoint *access_point = NULL;
808+ g_autofree char* full_key = NULL;
809++ g_autofree char* escaped_key = NULL;
810+ yaml_node_t* key, *value;
811+ const gchar* ssid;
812+
813+@@ -1442,13 +1462,15 @@ handle_wifi_access_points(NetplanParser* npp, yaml_node_t* node, const char* key
814+ value = yaml_document_get_node(&npp->doc, entry->value);
815+ assert_type(npp, value, YAML_MAPPING_NODE);
816+
817++ escaped_key = g_strescape(scalar(key), STRESCAPE_EXCEPTIONS);
818++
819+ if (key_prefix && npp->null_fields) {
820+- full_key = g_strdup_printf("%s\t%s", key_prefix, key->data.scalar.value);
821++ full_key = g_strdup_printf("%s\t%s", key_prefix, escaped_key);
822+ if (g_hash_table_contains(npp->null_fields, full_key))
823+ continue;
824+ }
825+
826+- ssid = scalar(key);
827++ ssid = escaped_key;
828+
829+ /*
830+ * Delete the access-point if it already exists in the netdef and let the new
831+@@ -1606,15 +1628,16 @@ handle_nameservers_search(NetplanParser* npp, yaml_node_t* node, __unused const
832+ for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) {
833+ yaml_node_t *entry = yaml_document_get_node(&npp->doc, *i);
834+ assert_type(npp, entry, YAML_SCALAR_NODE);
835++ g_autofree char* escaped_entry = g_strescape(scalar(entry), STRESCAPE_EXCEPTIONS);
836+
837+ if (!npp->current.netdef->search_domains)
838+ npp->current.netdef->search_domains = g_array_new(FALSE, FALSE, sizeof(char*));
839+
840+- if (!is_string_in_array(npp->current.netdef->search_domains, scalar(entry))) {
841+- char* s = g_strdup(scalar(entry));
842++ if (!is_string_in_array(npp->current.netdef->search_domains, escaped_entry)) {
843++ char* s = g_strdup(escaped_entry);
844+ g_array_append_val(npp->current.netdef->search_domains, s);
845+ } else {
846+- g_debug("%s: Search domain '%s' has already been added", npp->current.netdef->id, scalar(entry));
847++ g_debug("%s: Search domain '%s' has already been added", npp->current.netdef->id, escaped_entry);
848+ }
849+ }
850+ mark_data_as_dirty(npp, &npp->current.netdef->search_domains);
851+@@ -2721,19 +2744,19 @@ handle_ovs_bridge_controller_addresses(NetplanParser* npp, yaml_node_t* node, __
852+
853+ /* Format: [p]unix:file */
854+ if (is_unix && vec[1] != NULL && vec[2] == NULL) {
855+- char* s = g_strdup(scalar(entry));
856++ char* s = g_strescape(scalar(entry), STRESCAPE_EXCEPTIONS);
857+ g_array_append_val(npp->current.netdef->ovs_settings.controller.addresses, s);
858+ g_strfreev(vec);
859+ continue;
860+ /* Format tcp:host[:port] or ssl:host[:port] */
861+ } else if (is_host && validate_ovs_target(TRUE, vec[1])) {
862+- char* s = g_strdup(scalar(entry));
863++ char* s = g_strescape(scalar(entry), STRESCAPE_EXCEPTIONS);
864+ g_array_append_val(npp->current.netdef->ovs_settings.controller.addresses, s);
865+ g_strfreev(vec);
866+ continue;
867+ /* Format ptcp:[port][:host] or pssl:[port][:host] */
868+ } else if (is_port && validate_ovs_target(FALSE, vec[1])) {
869+- char* s = g_strdup(scalar(entry));
870++ char* s = g_strescape(scalar(entry), STRESCAPE_EXCEPTIONS);
871+ g_array_append_val(npp->current.netdef->ovs_settings.controller.addresses, s);
872+ g_strfreev(vec);
873+ continue;
874+@@ -3045,6 +3068,8 @@ handle_network_ovs_settings_global_ports(NetplanParser* npp, yaml_node_t* node,
875+ NetplanNetDefinition *component2 = NULL;
876+
877+ for (yaml_node_item_t *iter = node->data.sequence.items.start; iter < node->data.sequence.items.top; iter++) {
878++ g_autofree char* escaped_port = NULL;
879++ g_autofree char* escaped_peer = NULL;
880+ pair = yaml_document_get_node(&npp->doc, *iter);
881+ assert_type(npp, pair, YAML_SEQUENCE_NODE);
882+
883+@@ -3059,11 +3084,14 @@ handle_network_ovs_settings_global_ports(NetplanParser* npp, yaml_node_t* node,
884+ peer = yaml_document_get_node(&npp->doc, *(item+1));
885+ assert_type(npp, peer, YAML_SCALAR_NODE);
886+
887++ escaped_port = g_strescape(scalar(port), STRESCAPE_EXCEPTIONS);
888++ escaped_peer = g_strescape(scalar(peer), STRESCAPE_EXCEPTIONS);
889++
890+ /* Create port 1 netdef */
891+- component1 = npp->parsed_defs ? g_hash_table_lookup(npp->parsed_defs, scalar(port)) : NULL;
892++ component1 = npp->parsed_defs ? g_hash_table_lookup(npp->parsed_defs, escaped_port) : NULL;
893+ if (!component1) {
894+- component1 = netplan_netdef_new(npp, scalar(port), NETPLAN_DEF_TYPE_PORT, NETPLAN_BACKEND_OVS);
895+- if (g_hash_table_remove(npp->missing_id, scalar(port)))
896++ component1 = netplan_netdef_new(npp, escaped_port, NETPLAN_DEF_TYPE_PORT, NETPLAN_BACKEND_OVS);
897++ if (g_hash_table_remove(npp->missing_id, escaped_port))
898+ npp->missing_ids_found++;
899+ }
900+
901+@@ -3074,15 +3102,15 @@ handle_network_ovs_settings_global_ports(NetplanParser* npp, yaml_node_t* node,
902+ component1->filepath = g_strdup(npp->current.filepath);
903+ }
904+
905+- if (component1->peer && g_strcmp0(component1->peer, scalar(peer)))
906++ if (component1->peer && g_strcmp0(component1->peer, escaped_peer))
907+ return yaml_error(npp, port, error, "Open vSwitch port '%s' is already assigned to peer '%s'",
908+ component1->id, component1->peer);
909+
910+ /* Create port 2 (peer) netdef */
911+- component2 = npp->parsed_defs ? g_hash_table_lookup(npp->parsed_defs, scalar(peer)) : NULL;
912++ component2 = npp->parsed_defs ? g_hash_table_lookup(npp->parsed_defs, escaped_peer) : NULL;
913+ if (!component2) {
914+- component2 = netplan_netdef_new(npp, scalar(peer), NETPLAN_DEF_TYPE_PORT, NETPLAN_BACKEND_OVS);
915+- if (g_hash_table_remove(npp->missing_id, scalar(peer)))
916++ component2 = netplan_netdef_new(npp, escaped_peer, NETPLAN_DEF_TYPE_PORT, NETPLAN_BACKEND_OVS);
917++ if (g_hash_table_remove(npp->missing_id, escaped_peer))
918+ npp->missing_ids_found++;
919+ }
920+
921+@@ -3093,16 +3121,16 @@ handle_network_ovs_settings_global_ports(NetplanParser* npp, yaml_node_t* node,
922+ component2->filepath = g_strdup(npp->current.filepath);
923+ }
924+
925+- if (component2->peer && g_strcmp0(component2->peer, scalar(port)))
926++ if (component2->peer && g_strcmp0(component2->peer, escaped_port))
927+ return yaml_error(npp, peer, error, "Open vSwitch port '%s' is already assigned to peer '%s'",
928+ component2->id, component2->peer);
929+
930+ if (!component1->peer) {
931+- component1->peer = g_strdup(scalar(peer));
932++ component1->peer = g_strdup(escaped_peer);
933+ component1->peer_link = component2;
934+ }
935+ if (!component2->peer) {
936+- component2->peer = g_strdup(scalar(port));
937++ component2->peer = g_strdup(escaped_port);
938+ component2->peer_link = component1;
939+ }
940+ }
941+diff --git a/src/util-internal.h b/src/util-internal.h
942+index b30745b..f6f36fc 100644
943+--- a/src/util-internal.h
944++++ b/src/util-internal.h
945+@@ -182,3 +182,6 @@ _netplan_netdef_pertype_iter_next(struct netdef_pertype_iter* it);
946+
947+ NETPLAN_INTERNAL void
948+ _netplan_netdef_pertype_iter_free(struct netdef_pertype_iter* it);
949++
950++gchar*
951++_netplan_scrub_string(const char* content);
952+diff --git a/src/util.c b/src/util.c
953+index edecdab..d8d3a57 100644
954+--- a/src/util.c
955++++ b/src/util.c
956+@@ -1223,3 +1223,14 @@ _is_auth_key_management_psk(const NetplanAuthenticationSettings* auth)
957+ return ( auth->key_management == NETPLAN_AUTH_KEY_MANAGEMENT_WPA_PSK
958+ || auth->key_management == NETPLAN_AUTH_KEY_MANAGEMENT_WPA_SAE);
959+ }
960++
961++gchar*
962++_netplan_scrub_string(const char* content)
963++{
964++ GString* s = g_string_new(content);
965++
966++ // Escape double quotes
967++ g_string_replace(s, "\"", "\\\"", 0);
968++
969++ return g_string_free(s, FALSE);
970++}
971+diff --git a/tests/generator/test_auth.py b/tests/generator/test_auth.py
972+index d3d886c..bf0108a 100644
973+--- a/tests/generator/test_auth.py
974++++ b/tests/generator/test_auth.py
975+@@ -92,21 +92,21 @@ class TestNetworkd(TestBase):
976+ self.assertIn('ctrl_interface=/run/wpa_supplicant', new_config)
977+ self.assertIn('''
978+ network={
979+- ssid="peer2peer"
980++ ssid=P"peer2peer"
981+ mode=1
982+ key_mgmt=NONE
983+ }
984+ ''', new_config)
985+ self.assertIn('''
986+ network={
987+- ssid="Luke's Home"
988++ ssid=P"Luke's Home"
989+ key_mgmt=WPA-PSK
990+ psk="4lsos3kr1t"
991+ }
992+ ''', new_config)
993+ self.assertIn('''
994+ network={
995+- ssid="BobsHome"
996++ ssid=P"BobsHome"
997+ key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
998+ ieee80211w=1
999+ psk=e03ce667c87bc81ca968d9120ca37f89eb09aec3c55b80386e5d772efd6b926e
1000+@@ -114,14 +114,14 @@ network={
1001+ ''', new_config)
1002+ self.assertIn('''
1003+ network={
1004+- ssid="BillsHome"
1005++ ssid=P"BillsHome"
1006+ key_mgmt=WPA-PSK
1007+ psk=db3b0acf5653aeaddd5fe034fb9f07175b2864f847b005aaa2f09182d9411b04
1008+ }
1009+ ''', new_config)
1010+ self.assertIn('''
1011+ network={
1012+- ssid="workplace2"
1013++ ssid=P"workplace2"
1014+ key_mgmt=WPA-EAP
1015+ eap=PEAP
1016+ identity="joe@internal.example.com"
1017+@@ -131,7 +131,7 @@ network={
1018+ ''', new_config)
1019+ self.assertIn('''
1020+ network={
1021+- ssid="workplace"
1022++ ssid=P"workplace"
1023+ key_mgmt=WPA-EAP
1024+ eap=TTLS
1025+ identity="joe@internal.example.com"
1026+@@ -141,7 +141,7 @@ network={
1027+ ''', new_config)
1028+ self.assertIn('''
1029+ network={
1030+- ssid="workplacehashed"
1031++ ssid=P"workplacehashed"
1032+ key_mgmt=WPA-EAP
1033+ eap=TTLS
1034+ identity="joe@internal.example.com"
1035+@@ -151,7 +151,7 @@ network={
1036+ ''', new_config)
1037+ self.assertIn('''
1038+ network={
1039+- ssid="customernet"
1040++ ssid=P"customernet"
1041+ key_mgmt=WPA-EAP
1042+ eap=TLS
1043+ identity="cert-joe@cust.example.com"
1044+@@ -164,13 +164,13 @@ network={
1045+ ''', new_config)
1046+ self.assertIn('''
1047+ network={
1048+- ssid="opennet"
1049++ ssid=P"opennet"
1050+ key_mgmt=NONE
1051+ }
1052+ ''', new_config)
1053+ self.assertIn('''
1054+ network={
1055+- ssid="Joe's Home"
1056++ ssid=P"Joe's Home"
1057+ key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
1058+ ieee80211w=1
1059+ psk="s0s3kr1t"
1060+diff --git a/tests/generator/test_common.py b/tests/generator/test_common.py
1061+index 785f6f4..3aab3f7 100644
1062+--- a/tests/generator/test_common.py
1063++++ b/tests/generator/test_common.py
1064+@@ -19,7 +19,7 @@
1065+ import os
1066+ import textwrap
1067+
1068+-from .base import TestBase, ND_DHCP4, ND_DHCP6, ND_DHCPYES, ND_EMPTY, NM_MANAGED, NM_UNMANAGED
1069++from .base import UDEV_NO_MAC_RULE, TestBase, ND_DHCP4, ND_DHCP6, ND_DHCPYES, ND_EMPTY, NM_MANAGED, NM_UNMANAGED
1070+
1071+
1072+ class TestNetworkd(TestBase):
1073+@@ -815,6 +815,18 @@ RouteMetric=100
1074+ UseMTU=true
1075+ '''})
1076+
1077++ def test_nd_udev_rules_escaped(self):
1078++ self.generate('''network:
1079++ version: 2
1080++ renderer: NetworkManager
1081++ ethernets:
1082++ def1:
1083++ match:
1084++ driver: "abc\\"xyz\\n0\\n\\n1"
1085++ set-name: "eth\\"\\n\\nxyz\\n0"''', skip_generated_yaml_validation=True)
1086++
1087++ self.assert_networkd_udev({'def1.rules': (UDEV_NO_MAC_RULE % ('abc\\"xyz\\n0\\n\\n1', 'eth\\"\\n\\nxyz\\n0'))})
1088++
1089+
1090+ class TestNetworkManager(TestBase):
1091+
1092+@@ -1320,6 +1332,28 @@ dns=8.8.8.8;
1093+ method=ignore
1094+ '''})
1095+
1096++ def test_nm_udev_rules_escaped(self):
1097++ self.generate('''network:
1098++ version: 2
1099++ renderer: networkd
1100++ ethernets:
1101++ eth0:
1102++ match:
1103++ name: "eth\\"0"
1104++ dhcp4: true''')
1105++ self.assert_nm_udev(NM_UNMANAGED % 'eth\\"0')
1106++
1107++ self.generate('''network:
1108++ version: 2
1109++ renderer: networkd
1110++ ethernets:
1111++ eth0:
1112++ match:
1113++ name: "eth0"
1114++ set-name: "eth\\n0"
1115++ dhcp4: true''', skip_generated_yaml_validation=True)
1116++ self.assert_nm_udev(NM_UNMANAGED % 'eth\\n0' + NM_UNMANAGED % 'eth0')
1117++
1118+
1119+ class TestForwardDeclaration(TestBase):
1120+
1121+@@ -1782,13 +1816,13 @@ LinkLocalAddressing=ipv6
1122+ self.assert_wpa_supplicant("wlan0", """ctrl_interface=/run/wpa_supplicant
1123+
1124+ network={
1125+- ssid="mynewwifi"
1126++ ssid=P"mynewwifi"
1127+ key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
1128+ ieee80211w=1
1129+ psk="aaaaaaaa"
1130+ }
1131+ network={
1132+- ssid="mywifi"
1133++ ssid=P"mywifi"
1134+ key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
1135+ ieee80211w=1
1136+ psk="aaaaaaaa"
1137+@@ -1819,7 +1853,7 @@ network={
1138+ self.assert_wpa_supplicant("wlan0", """ctrl_interface=/run/wpa_supplicant
1139+
1140+ network={
1141+- ssid="mywifi"
1142++ ssid=P"mywifi"
1143+ key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
1144+ ieee80211w=1
1145+ psk="bbbbbbbb"
1146+diff --git a/tests/generator/test_wifis.py b/tests/generator/test_wifis.py
1147+index 610782a..9f7359b 100644
1148+--- a/tests/generator/test_wifis.py
1149++++ b/tests/generator/test_wifis.py
1150+@@ -65,7 +65,7 @@ class TestNetworkd(TestBase):
1151+ with open(os.path.join(self.workdir.name, 'run/netplan/wpa-wl0.conf')) as f:
1152+ new_config = f.read()
1153+
1154+- network = 'ssid="{}"\n freq_list='.format('band-no-channel2')
1155++ network = 'ssid=P"{}"\n freq_list='.format('band-no-channel2')
1156+ freqs_5GHz = [5610, 5310, 5620, 5320, 5630, 5640, 5340, 5035, 5040, 5045, 5055, 5060, 5660, 5680, 5670, 5080, 5690,
1157+ 5700, 5710, 5720, 5825, 5745, 5755, 5805, 5765, 5160, 5775, 5170, 5480, 5180, 5795, 5190, 5500, 5200,
1158+ 5510, 5210, 5520, 5220, 5530, 5230, 5540, 5240, 5550, 5250, 5560, 5260, 5570, 5270, 5580, 5280, 5590,
1159+@@ -76,7 +76,7 @@ class TestNetworkd(TestBase):
1160+ for freq in freqs_5GHz:
1161+ self.assertRegex(new_config, '{}[ 0-9]*{}[ 0-9]*\n'.format(network, freq))
1162+
1163+- network = 'ssid="{}"\n freq_list='.format('band-no-channel')
1164++ network = 'ssid=P"{}"\n freq_list='.format('band-no-channel')
1165+ freqs_24GHz = [2412, 2417, 2422, 2427, 2432, 2442, 2447, 2437, 2452, 2457, 2462, 2467, 2472, 2484]
1166+ freqs = new_config.split(network)
1167+ freqs = freqs[1].split('\n')[0]
1168+@@ -86,20 +86,20 @@ class TestNetworkd(TestBase):
1169+
1170+ self.assertIn('''
1171+ network={
1172+- ssid="channel-no-band"
1173++ ssid=P"channel-no-band"
1174+ key_mgmt=NONE
1175+ }
1176+ ''', new_config)
1177+ self.assertIn('''
1178+ network={
1179+- ssid="peer2peer"
1180++ ssid=P"peer2peer"
1181+ mode=1
1182+ key_mgmt=NONE
1183+ }
1184+ ''', new_config)
1185+ self.assertIn('''
1186+ network={
1187+- ssid="hidden-y"
1188++ ssid=P"hidden-y"
1189+ scan_ssid=1
1190+ key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
1191+ ieee80211w=1
1192+@@ -108,7 +108,7 @@ network={
1193+ ''', new_config)
1194+ self.assertIn('''
1195+ network={
1196+- ssid="hidden-n"
1197++ ssid=P"hidden-n"
1198+ key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
1199+ ieee80211w=1
1200+ psk="5ecur1ty"
1201+@@ -116,7 +116,7 @@ network={
1202+ ''', new_config)
1203+ self.assertIn('''
1204+ network={
1205+- ssid="workplace"
1206++ ssid=P"workplace"
1207+ bssid=de:ad:be:ef:ca:fe
1208+ freq_list=5500
1209+ key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
1210+@@ -126,7 +126,7 @@ network={
1211+ ''', new_config)
1212+ self.assertIn('''
1213+ network={
1214+- ssid="Joe's Home"
1215++ ssid=P"Joe's Home"
1216+ bssid=00:11:22:33:44:55
1217+ freq_list=2462
1218+ key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
1219+@@ -303,7 +303,7 @@ LinkLocalAddressing=ipv6
1220+ self.assertIn('''
1221+ wowlan_triggers=any disconnect magic_pkt gtk_rekey_failure eap_identity_req four_way_handshake rfkill_release
1222+ network={
1223+- ssid="homenet"
1224++ ssid=P"homenet"
1225+ key_mgmt=NONE
1226+ }
1227+ ''', new_config)
1228+@@ -336,7 +336,7 @@ LinkLocalAddressing=ipv6
1229+ new_config = f.read()
1230+ self.assertIn('''
1231+ network={
1232+- ssid="homenet"
1233++ ssid=P"homenet"
1234+ key_mgmt=NONE
1235+ }
1236+ ''', new_config)
1237+@@ -360,7 +360,7 @@ network={
1238+ self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant
1239+
1240+ network={
1241+- ssid="homenet"
1242++ ssid=P"homenet"
1243+ key_mgmt=SAE
1244+ ieee80211w=2
1245+ psk="********"
1246+@@ -387,7 +387,7 @@ network={
1247+ self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant
1248+
1249+ network={
1250+- ssid="homenet"
1251++ ssid=P"homenet"
1252+ key_mgmt=WPA-EAP WPA-EAP-SHA256
1253+ eap=TLS
1254+ ieee80211w=1
1255+@@ -420,7 +420,7 @@ network={
1256+ self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant
1257+
1258+ network={
1259+- ssid="homenet"
1260++ ssid=P"homenet"
1261+ key_mgmt=WPA-EAP-SUITE-B-192
1262+ eap=TLS
1263+ ieee80211w=2
1264+@@ -449,7 +449,7 @@ network={
1265+ self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant
1266+
1267+ network={
1268+- ssid="homenet"
1269++ ssid=P"homenet"
1270+ key_mgmt=IEEE8021X
1271+ eap=LEAP
1272+ identity="some-id"
1273+@@ -473,7 +473,7 @@ network={
1274+ self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant
1275+
1276+ network={
1277+- ssid="homenet"
1278++ ssid=P"homenet"
1279+ key_mgmt=IEEE8021X
1280+ eap=PWD
1281+ identity="some-id"
1282+@@ -498,7 +498,7 @@ network={
1283+ self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant
1284+
1285+ network={
1286+- ssid="homenet"
1287++ ssid=P"homenet"
1288+ key_mgmt=WPA-EAP
1289+ eap=LEAP
1290+ ieee80211w=1
1291+@@ -508,6 +508,48 @@ network={
1292+ }
1293+ """)
1294+
1295++ def test_escaping_special_characters(self):
1296++ self.generate('''network:
1297++ version: 2
1298++ wifis:
1299++ wl0:
1300++ regulatory-domain: "abc\\n\\n321\\n\\"123"
1301++ access-points:
1302++ "abc\\n\\n123\\"x\\ry\\bz":
1303++ password: "abc\\n\\n\\n\\"123"
1304++ auth:
1305++ key-management: eap
1306++ method: leap
1307++ anonymous-identity: "abc\\n\\n321\\n\\"123"
1308++ identity: "abc\\n\\n321\\n\\"123"
1309++ password: "abc\\n\\n\\n\\"123"
1310++ ca-certificate: "abc\\n\\n321\\n\\"123"
1311++ client-certificate: "abc\\n\\n321\\n\\"123"
1312++ client-key: "abc\\n\\n321\\n\\"123"
1313++ client-key-password: "abc\\n\\n321\\n\\"123"
1314++ phase2-auth: "abc\\n\\n321\\n\\"123"
1315++ ''', skip_generated_yaml_validation=True)
1316++
1317++ self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant
1318++
1319++country=abc\\n\\n321\\n\\"123
1320++network={
1321++ ssid=P"abc\\n\\n123\\\"x\\ry\\bz"
1322++ key_mgmt=WPA-EAP
1323++ eap=LEAP
1324++ ieee80211w=1
1325++ identity="abc\\n\\n321\\n\\\"123"
1326++ anonymous_identity="abc\\n\\n321\\n\\\"123"
1327++ psk="abc\\n\\n\\n\"123"
1328++ password="abc\\n\\n\\n\"123"
1329++ ca_cert="abc\\n\\n321\\n\\\"123"
1330++ client_cert="abc\\n\\n321\\n\\\"123"
1331++ private_key="abc\\n\\n321\\n\\\"123"
1332++ private_key_passwd="abc\\n\\n321\\n\"123"
1333++ phase2="auth=abc\\n\\n321\\n\\\"123"
1334++}
1335++""")
1336++
1337+
1338+ class TestNetworkManager(TestBase):
1339+
1340+@@ -790,7 +832,7 @@ mode=adhoc
1341+ self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant
1342+
1343+ network={
1344+- ssid="homenet"
1345++ ssid=P"homenet"
1346+ frequency=2442
1347+ mode=1
1348+ key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
1349+@@ -814,7 +856,7 @@ network={
1350+ self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant
1351+
1352+ network={
1353+- ssid="homenet"
1354++ ssid=P"homenet"
1355+ frequency=5035
1356+ mode=1
1357+ key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
1358diff --git a/debian/patches/lp2066258/0015-backends-escape-file-paths.patch b/debian/patches/lp2066258/0015-backends-escape-file-paths.patch
1359new file mode 100644
1360index 0000000..606381e
1361--- /dev/null
1362+++ b/debian/patches/lp2066258/0015-backends-escape-file-paths.patch
1363@@ -0,0 +1,288 @@
1364+From: Danilo Egea Gondolfo <danilogondolfo@gmail.com>
1365+Date: Thu, 23 May 2024 15:54:43 +0100
1366+Subject: backends: escape file paths
1367+
1368+Escape strings used to build paths with g_uri_escape_string().
1369+systemd_escape() could also be used but it has the downside of calling
1370+an external program and, by default, it escapes dashes (which are
1371+present in files generated from Network Manager for example).
1372+---
1373+ src/networkd.c | 13 +++++----
1374+ src/nm.c | 5 ++--
1375+ src/openvswitch.c | 19 ++++++------
1376+ src/util.c | 7 +++--
1377+ tests/generator/test_common.py | 66 ++++++++++++++++++++++++++++++++++++++++++
1378+ tests/generator/test_ovs.py | 24 +++++++++++++++
1379+ 6 files changed, 115 insertions(+), 19 deletions(-)
1380+
1381+diff --git a/src/networkd.c b/src/networkd.c
1382+index ac54112..e66ab4c 100644
1383+--- a/src/networkd.c
1384++++ b/src/networkd.c
1385+@@ -981,7 +981,8 @@ static void
1386+ write_rules_file(const NetplanNetDefinition* def, const char* rootdir)
1387+ {
1388+ GString* s = NULL;
1389+- g_autofree char* path = g_strjoin(NULL, "run/udev/rules.d/99-netplan-", def->id, ".rules", NULL);
1390++ g_autofree char* escaped_netdef_id = g_uri_escape_string(def->id, NULL, TRUE);
1391++ g_autofree char* path = g_strjoin(NULL, "run/udev/rules.d/99-netplan-", escaped_netdef_id, ".rules", NULL);
1392+
1393+ /* do we need to write a .rules file?
1394+ * It's only required for reliably setting the name of a physical device
1395+@@ -1198,7 +1199,8 @@ write_wpa_conf(const NetplanNetDefinition* def, const char* rootdir, GError** er
1396+ {
1397+ GHashTableIter iter;
1398+ GString* s = g_string_new("ctrl_interface=/run/wpa_supplicant\n\n");
1399+- g_autofree char* path = g_strjoin(NULL, "run/netplan/wpa-", def->id, ".conf", NULL);
1400++ g_autofree char* escaped_netdef_id = g_uri_escape_string(def->id, NULL, TRUE);
1401++ g_autofree char* path = g_strjoin(NULL, "run/netplan/wpa-", escaped_netdef_id, ".conf", NULL);
1402+
1403+ g_debug("%s: Creating wpa_supplicant configuration file %s", def->id, path);
1404+ if (def->type == NETPLAN_DEF_TYPE_WIFI) {
1405+@@ -1310,7 +1312,8 @@ netplan_netdef_write_networkd(
1406+ GError** error)
1407+ {
1408+ /* TODO: make use of netplan_netdef_get_output_filename() */
1409+- g_autofree char* path_base = g_strjoin(NULL, "run/systemd/network/10-netplan-", def->id, NULL);
1410++ g_autofree char* escaped_netdef_id = g_uri_escape_string(def->id, NULL, TRUE);
1411++ g_autofree char* path_base = g_strjoin(NULL, "run/systemd/network/10-netplan-", escaped_netdef_id, NULL);
1412+ SET_OPT_OUT_PTR(has_been_written, FALSE);
1413+
1414+ /* We want this for all backends when renaming, as *.link and *.rules files are
1415+@@ -1332,8 +1335,8 @@ netplan_netdef_write_networkd(
1416+ }
1417+
1418+ if (def->type == NETPLAN_DEF_TYPE_WIFI || def->has_auth) {
1419+- g_autofree char* link = g_strjoin(NULL, rootdir ?: "", "/run/systemd/system/systemd-networkd.service.wants/netplan-wpa-", def->id, ".service", NULL);
1420+- g_autofree char* slink = g_strjoin(NULL, "/run/systemd/system/netplan-wpa-", def->id, ".service", NULL);
1421++ g_autofree char* link = g_strjoin(NULL, rootdir ?: "", "/run/systemd/system/systemd-networkd.service.wants/netplan-wpa-", escaped_netdef_id, ".service", NULL);
1422++ g_autofree char* slink = g_strjoin(NULL, "/run/systemd/system/netplan-wpa-", escaped_netdef_id, ".service", NULL);
1423+ if (def->type == NETPLAN_DEF_TYPE_WIFI && def->has_match) {
1424+ g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_UNSUPPORTED, "ERROR: %s: networkd backend does not support wifi with match:, only by interface name\n", def->id);
1425+ return FALSE;
1426+diff --git a/src/nm.c b/src/nm.c
1427+index a605c38..380b7fd 100644
1428+--- a/src/nm.c
1429++++ b/src/nm.c
1430+@@ -931,10 +931,11 @@ write_nm_conf_access_point(const NetplanNetDefinition* def, const char* rootdir,
1431+ g_datalist_foreach((GData**)&def->backend_settings.passthrough, write_fallback_key_value, kf);
1432+ }
1433+
1434++ g_autofree char* escaped_netdef_id = g_uri_escape_string(def->id, NULL, TRUE);
1435+ if (ap) {
1436+ g_autofree char* escaped_ssid = g_uri_escape_string(ap->ssid, NULL, TRUE);
1437+ /* TODO: make use of netplan_netdef_get_output_filename() */
1438+- conf_path = g_strjoin(NULL, "run/NetworkManager/system-connections/netplan-", def->id, "-", escaped_ssid, ".nmconnection", NULL);
1439++ conf_path = g_strjoin(NULL, "run/NetworkManager/system-connections/netplan-", escaped_netdef_id, "-", escaped_ssid, ".nmconnection", NULL);
1440+
1441+ g_key_file_set_string(kf, "wifi", "ssid", ap->ssid);
1442+ if (ap->mode < NETPLAN_WIFI_MODE_OTHER)
1443+@@ -969,7 +970,7 @@ write_nm_conf_access_point(const NetplanNetDefinition* def, const char* rootdir,
1444+ }
1445+ } else {
1446+ /* TODO: make use of netplan_netdef_get_output_filename() */
1447+- conf_path = g_strjoin(NULL, "run/NetworkManager/system-connections/netplan-", def->id, ".nmconnection", NULL);
1448++ conf_path = g_strjoin(NULL, "run/NetworkManager/system-connections/netplan-", escaped_netdef_id, ".nmconnection", NULL);
1449+ if (def->has_auth) {
1450+ write_dot1x_auth_parameters(&def->auth, kf);
1451+ }
1452+diff --git a/src/openvswitch.c b/src/openvswitch.c
1453+index 276762e..b33769d 100644
1454+--- a/src/openvswitch.c
1455++++ b/src/openvswitch.c
1456+@@ -32,9 +32,9 @@
1457+ static gboolean
1458+ write_ovs_systemd_unit(const char* id, const GString* cmds, const char* rootdir, gboolean physical, gboolean cleanup, const char* dependency, GError** error)
1459+ {
1460+- g_autofree gchar* id_escaped = NULL;
1461+- g_autofree char* link = g_strjoin(NULL, rootdir ?: "", "/run/systemd/system/systemd-networkd.service.wants/netplan-ovs-", id, ".service", NULL);
1462+- g_autofree char* path = g_strjoin(NULL, "/run/systemd/system/netplan-ovs-", id, ".service", NULL);
1463++ g_autofree char* escaped_netdef_id = g_uri_escape_string(id, NULL, TRUE);
1464++ g_autofree char* link = g_strjoin(NULL, rootdir ?: "", "/run/systemd/system/systemd-networkd.service.wants/netplan-ovs-", escaped_netdef_id, ".service", NULL);
1465++ g_autofree char* path = g_strjoin(NULL, "/run/systemd/system/netplan-ovs-", escaped_netdef_id, ".service", NULL);
1466+
1467+ GString* s = g_string_new("[Unit]\n");
1468+ g_string_append_printf(s, "Description=OpenVSwitch configuration for %s\n", id);
1469+@@ -43,9 +43,8 @@ write_ovs_systemd_unit(const char* id, const GString* cmds, const char* rootdir,
1470+ g_string_append_printf(s, "Wants=ovsdb-server.service\n");
1471+ g_string_append_printf(s, "After=ovsdb-server.service\n");
1472+ if (physical) {
1473+- id_escaped = systemd_escape((char*) id);
1474+- g_string_append_printf(s, "Requires=sys-subsystem-net-devices-%s.device\n", id_escaped);
1475+- g_string_append_printf(s, "After=sys-subsystem-net-devices-%s.device\n", id_escaped);
1476++ g_string_append_printf(s, "Requires=sys-subsystem-net-devices-%s.device\n", escaped_netdef_id);
1477++ g_string_append_printf(s, "After=sys-subsystem-net-devices-%s.device\n", escaped_netdef_id);
1478+ }
1479+ if (!cleanup) {
1480+ g_string_append_printf(s, "After=netplan-ovs-cleanup.service\n");
1481+@@ -55,8 +54,9 @@ write_ovs_systemd_unit(const char* id, const GString* cmds, const char* rootdir,
1482+ }
1483+ g_string_append(s, "Before=network.target\nWants=network.target\n");
1484+ if (dependency) {
1485+- g_string_append_printf(s, "Requires=netplan-ovs-%s.service\n", dependency);
1486+- g_string_append_printf(s, "After=netplan-ovs-%s.service\n", dependency);
1487++ g_autofree char* escaped_dependency = g_uri_escape_string(dependency, NULL, TRUE);
1488++ g_string_append_printf(s, "Requires=netplan-ovs-%s.service\n", escaped_dependency);
1489++ g_string_append_printf(s, "After=netplan-ovs-%s.service\n", escaped_dependency);
1490+ }
1491+
1492+ g_string_append(s, "\n[Service]\nType=oneshot\nTimeoutStartSec=10s\n");
1493+@@ -321,6 +321,7 @@ netplan_netdef_write_ovs(const NetplanState* np_state, const NetplanNetDefinitio
1494+ gchar* dependency = NULL;
1495+ const char* type = netplan_type_to_table_name(def->type);
1496+ g_autofree char* base_config_path = NULL;
1497++ g_autofree char* escaped_netdef_id = g_uri_escape_string(def->id, NULL, TRUE);
1498+ char* value = NULL;
1499+ const NetplanOVSSettings* settings = &np_state->ovs_settings;
1500+
1501+@@ -411,7 +412,7 @@ netplan_netdef_write_ovs(const NetplanState* np_state, const NetplanNetDefinitio
1502+
1503+ /* Try writing out a base config */
1504+ /* TODO: make use of netplan_netdef_get_output_filename() */
1505+- base_config_path = g_strjoin(NULL, "run/systemd/network/10-netplan-", def->id, NULL);
1506++ base_config_path = g_strjoin(NULL, "run/systemd/network/10-netplan-", escaped_netdef_id, NULL);
1507+ if (!netplan_netdef_write_network_file(np_state, def, rootdir, base_config_path, has_been_written, error))
1508+ return FALSE;
1509+ } else {
1510+diff --git a/src/util.c b/src/util.c
1511+index d8d3a57..a3dae98 100644
1512+--- a/src/util.c
1513++++ b/src/util.c
1514+@@ -691,17 +691,18 @@ ssize_t
1515+ netplan_netdef_get_output_filename(const NetplanNetDefinition* netdef, const char* ssid, char* out_buffer, size_t out_buf_size)
1516+ {
1517+ g_autofree gchar* conf_path = NULL;
1518++ g_autofree char* escaped_netdef_id = g_uri_escape_string(netdef->id, NULL, TRUE);
1519+
1520+ if (netdef->backend == NETPLAN_BACKEND_NM) {
1521+ if (ssid) {
1522+ g_autofree char* escaped_ssid = g_uri_escape_string(ssid, NULL, TRUE);
1523+- conf_path = g_strjoin(NULL, "/run/NetworkManager/system-connections/netplan-", netdef->id, "-", escaped_ssid, ".nmconnection", NULL);
1524++ conf_path = g_strjoin(NULL, "/run/NetworkManager/system-connections/netplan-", escaped_netdef_id, "-", escaped_ssid, ".nmconnection", NULL);
1525+ } else {
1526+- conf_path = g_strjoin(NULL, "/run/NetworkManager/system-connections/netplan-", netdef->id, ".nmconnection", NULL);
1527++ conf_path = g_strjoin(NULL, "/run/NetworkManager/system-connections/netplan-", escaped_netdef_id, ".nmconnection", NULL);
1528+ }
1529+
1530+ } else if (netdef->backend == NETPLAN_BACKEND_NETWORKD || netdef->backend == NETPLAN_BACKEND_OVS) {
1531+- conf_path = g_strjoin(NULL, "/run/systemd/network/10-netplan-", netdef->id, ".network", NULL);
1532++ conf_path = g_strjoin(NULL, "/run/systemd/network/10-netplan-", escaped_netdef_id, ".network", NULL);
1533+ }
1534+
1535+ if (conf_path)
1536+diff --git a/tests/generator/test_common.py b/tests/generator/test_common.py
1537+index 3aab3f7..f145104 100644
1538+--- a/tests/generator/test_common.py
1539++++ b/tests/generator/test_common.py
1540+@@ -827,6 +827,47 @@ UseMTU=true
1541+
1542+ self.assert_networkd_udev({'def1.rules': (UDEV_NO_MAC_RULE % ('abc\\"xyz\\n0\\n\\n1', 'eth\\"\\n\\nxyz\\n0'))})
1543+
1544++ def test_nd_file_paths_escaped(self):
1545++ self.generate('''network:
1546++ version: 2
1547++ ethernets:
1548++ "abc/../../xyz0":
1549++ match:
1550++ driver: "drv"
1551++ set-name: "eth123"''')
1552++
1553++ self.assert_networkd_udev({'abc%2F..%2F..%2Fxyz0.rules': (UDEV_NO_MAC_RULE % ('drv', 'eth123'))})
1554++ self.assert_networkd({'abc%2F..%2F..%2Fxyz0.network': '''[Match]\nDriver=drv
1555++Name=eth123
1556++
1557++[Network]
1558++LinkLocalAddressing=ipv6
1559++''',
1560++ 'abc%2F..%2F..%2Fxyz0.link': '''[Match]\nDriver=drv\n
1561++[Link]
1562++Name=eth123
1563++WakeOnLan=off
1564++'''})
1565++
1566++ self.generate('''network:
1567++ version: 2
1568++ wifis:
1569++ "abc/../../xyz0":
1570++ dhcp4: true
1571++ access-points:
1572++ "mywifi":
1573++ password: "aaaaaaaa"''')
1574++
1575++ self.assert_wpa_supplicant("abc%2F..%2F..%2Fxyz0", """ctrl_interface=/run/wpa_supplicant
1576++
1577++network={
1578++ ssid=P"mywifi"
1579++ key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
1580++ ieee80211w=1
1581++ psk="aaaaaaaa"
1582++}
1583++""")
1584++
1585+
1586+ class TestNetworkManager(TestBase):
1587+
1588+@@ -1354,6 +1395,31 @@ method=ignore
1589+ dhcp4: true''', skip_generated_yaml_validation=True)
1590+ self.assert_nm_udev(NM_UNMANAGED % 'eth\\n0' + NM_UNMANAGED % 'eth0')
1591+
1592++ def test_nm_file_paths_escaped(self):
1593++ self.generate('''network:
1594++ version: 2
1595++ renderer: NetworkManager
1596++ ethernets:
1597++ "abc/../../xyz0":
1598++ match:
1599++ driver: "drv"
1600++ set-name: "eth123"''')
1601++
1602++ self.assert_nm({'abc%2F..%2F..%2Fxyz0': '''[connection]
1603++id=netplan-abc/../../xyz0
1604++type=ethernet
1605++interface-name=eth123
1606++
1607++[ethernet]
1608++wake-on-lan=0
1609++
1610++[ipv4]
1611++method=link-local
1612++
1613++[ipv6]
1614++method=ignore
1615++'''})
1616++
1617+
1618+ class TestForwardDeclaration(TestBase):
1619+
1620+diff --git a/tests/generator/test_ovs.py b/tests/generator/test_ovs.py
1621+index 307df5f..0d27ff9 100644
1622+--- a/tests/generator/test_ovs.py
1623++++ b/tests/generator/test_ovs.py
1624+@@ -1095,3 +1095,27 @@ ExecStart=/usr/bin/ovs-vsctl set-controller br123 tcp:127.0.0.1:6653\n\
1625+ ExecStart=/usr/bin/ovs-vsctl set Bridge br123 external-ids:netplan/global/set-controller=tcp:127.0.0.1:6653\n\
1626+ ')},
1627+ 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}})
1628++
1629++ def test_file_paths_escaped(self):
1630++ self.generate('''network:
1631++ version: 2
1632++ bridges:
1633++ "abc/../../123":
1634++ openvswitch: {}
1635++ vlans:
1636++ "abc/../../123.100":
1637++ id: 100
1638++ link: "abc/../../123"
1639++''')
1640++ self.assert_ovs({'abc%2F..%2F..%2F123.service': OVS_BR_EMPTY % {'iface': 'abc/../../123'},
1641++ 'abc%2F..%2F..%2F123.100.service': OVS_VIRTUAL % {'iface': 'abc/../../123.100', 'extra':
1642++ '''Requires=netplan-ovs-abc%2F..%2F..%2F123.service
1643++After=netplan-ovs-abc%2F..%2F..%2F123.service
1644++
1645++[Service]
1646++Type=oneshot
1647++TimeoutStartSec=10s
1648++ExecStart=/usr/bin/ovs-vsctl --may-exist add-br abc/../../123.100 abc/../../123 100
1649++ExecStart=/usr/bin/ovs-vsctl set Interface abc/../../123.100 external-ids:netplan=true
1650++'''},
1651++ 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}})
1652diff --git a/debian/patches/lp2066258/0016-backends-escape-semicolons-in-service-units.patch b/debian/patches/lp2066258/0016-backends-escape-semicolons-in-service-units.patch
1653new file mode 100644
1654index 0000000..ac83351
1655--- /dev/null
1656+++ b/debian/patches/lp2066258/0016-backends-escape-semicolons-in-service-units.patch
1657@@ -0,0 +1,292 @@
1658+From: Danilo Egea Gondolfo <danilogondolfo@gmail.com>
1659+Date: Fri, 24 May 2024 14:11:12 +0100
1660+Subject: backends: escape semicolons in service units
1661+
1662+Semicolons separated from other words by a combination of spaces and/or
1663+tabs will be escaped.
1664+---
1665+ src/networkd.c | 7 +++++
1666+ src/openvswitch.c | 3 +++
1667+ src/sriov.c | 3 +++
1668+ src/util-internal.h | 3 +++
1669+ src/util.c | 34 +++++++++++++++++++++++
1670+ tests/ctests/test_netplan_misc.c | 45 +++++++++++++++++++++++++++++++
1671+ tests/generator/test_ovs.py | 58 ++++++++++++++++++++++++++++++++++++++++
1672+ tests/test_sriov.py | 29 ++++++++++++++++++++
1673+ 8 files changed, 182 insertions(+)
1674+
1675+diff --git a/src/networkd.c b/src/networkd.c
1676+index e66ab4c..f9fefe9 100644
1677+--- a/src/networkd.c
1678++++ b/src/networkd.c
1679+@@ -301,6 +301,9 @@ write_regdom(const NetplanNetDefinition* def, const char* rootdir, GError** erro
1680+ g_string_append(s, "\n[Service]\nType=oneshot\n");
1681+ g_string_append_printf(s, "ExecStart="SBINDIR"/iw reg set %s\n", def->regulatory_domain);
1682+
1683++ g_autofree char* new_s = _netplan_scrub_systemd_unit_contents(s->str);
1684++ g_string_free(s, TRUE);
1685++ s = g_string_new(new_s);
1686+ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
1687+ safe_mkdir_p_dir(link);
1688+ if (symlink(path, link) < 0 && errno != EEXIST) {
1689+@@ -1191,6 +1194,10 @@ write_wpa_unit(const NetplanNetDefinition* def, const char* rootdir)
1690+ } else {
1691+ g_string_append(s, " -Dnl80211,wext\n");
1692+ }
1693++
1694++ g_autofree char* new_s = _netplan_scrub_systemd_unit_contents(s->str);
1695++ g_string_free(s, TRUE);
1696++ s = g_string_new(new_s);
1697+ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
1698+ }
1699+
1700+diff --git a/src/openvswitch.c b/src/openvswitch.c
1701+index b33769d..7d31e7f 100644
1702+--- a/src/openvswitch.c
1703++++ b/src/openvswitch.c
1704+@@ -62,6 +62,9 @@ write_ovs_systemd_unit(const char* id, const GString* cmds, const char* rootdir,
1705+ g_string_append(s, "\n[Service]\nType=oneshot\nTimeoutStartSec=10s\n");
1706+ g_string_append(s, cmds->str);
1707+
1708++ g_autofree char* new_s = _netplan_scrub_systemd_unit_contents(s->str);
1709++ g_string_free(s, TRUE);
1710++ s = g_string_new(new_s);
1711+ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
1712+
1713+ safe_mkdir_p_dir(link);
1714+diff --git a/src/sriov.c b/src/sriov.c
1715+index c3cd80d..710f1a8 100644
1716+--- a/src/sriov.c
1717++++ b/src/sriov.c
1718+@@ -53,6 +53,9 @@ write_sriov_rebind_systemd_unit(GHashTable* pfs, const char* rootdir, GError** e
1719+ g_string_truncate(interfaces, interfaces->len-1); /* cut trailing whitespace */
1720+ g_string_append_printf(s, "ExecStart=" SBINDIR "/netplan rebind %s\n", interfaces->str);
1721+
1722++ g_autofree char* new_s = _netplan_scrub_systemd_unit_contents(s->str);
1723++ g_string_free(s, TRUE);
1724++ s = g_string_new(new_s);
1725+ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
1726+ g_string_free(interfaces, TRUE);
1727+
1728+diff --git a/src/util-internal.h b/src/util-internal.h
1729+index f6f36fc..e9c2d13 100644
1730+--- a/src/util-internal.h
1731++++ b/src/util-internal.h
1732+@@ -185,3 +185,6 @@ _netplan_netdef_pertype_iter_free(struct netdef_pertype_iter* it);
1733+
1734+ gchar*
1735+ _netplan_scrub_string(const char* content);
1736++
1737++gchar*
1738++_netplan_scrub_systemd_unit_contents(const char* content);
1739+diff --git a/src/util.c b/src/util.c
1740+index a3dae98..30d05cb 100644
1741+--- a/src/util.c
1742++++ b/src/util.c
1743+@@ -1235,3 +1235,37 @@ _netplan_scrub_string(const char* content)
1744+
1745+ return g_string_free(s, FALSE);
1746+ }
1747++
1748++static gboolean
1749++_is_space_or_tab(char c)
1750++{
1751++ return c == ' ' || c == '\t';
1752++}
1753++
1754++char*
1755++_netplan_scrub_systemd_unit_contents(const char* content)
1756++{
1757++ size_t content_len = strlen(content);
1758++ // Assume a few replacements will happen to reduce reallocation
1759++ GString* s = g_string_sized_new(content_len + 8);
1760++
1761++ // Append the first character of "content" to the result string
1762++ g_string_append_len(s, content, 1);
1763++
1764++ // Walk from the second element to the one before the last looking for isolated semicolons
1765++ // A semicolon is isolated if it's surrounded by either tabs or spaces
1766++ const char* p = content + 1;
1767++ while (p < (content + content_len - 1)) {
1768++ if (*p == ';' && _is_space_or_tab(*(p - 1)) && _is_space_or_tab(*(p + 1))) {
1769++ g_string_append_len(s, "\\;", 2);
1770++ } else {
1771++ g_string_append_len(s, p, 1);
1772++ }
1773++ p++;
1774++ }
1775++
1776++ // Append the last character of "content" to the result string
1777++ g_string_append_len(s, p, 1);
1778++
1779++ return g_string_free(s, FALSE);
1780++}
1781+diff --git a/tests/ctests/test_netplan_misc.c b/tests/ctests/test_netplan_misc.c
1782+index 35bf4f8..aa14a71 100644
1783+--- a/tests/ctests/test_netplan_misc.c
1784++++ b/tests/ctests/test_netplan_misc.c
1785+@@ -348,6 +348,50 @@ test_normalize_ip_address(__unused void** state)
1786+ assert_string_equal(normalize_ip_address("0.0.0.0/0", AF_INET), "0.0.0.0/0");
1787+ }
1788+
1789++void
1790++test_scrub_systemd_unit_content(__unused void** state)
1791++{
1792++ char* str = ";abc;";
1793++ char* res = _netplan_scrub_systemd_unit_contents(str);
1794++ assert_string_equal(res, ";abc;");
1795++ g_free(res);
1796++
1797++ str = ";;;;";
1798++ res = _netplan_scrub_systemd_unit_contents(str);
1799++ assert_string_equal(res, ";;;;");
1800++ g_free(res);
1801++
1802++ str = " ;;;; ";
1803++ res = _netplan_scrub_systemd_unit_contents(str);
1804++ assert_string_equal(res, " ;;;; ");
1805++ g_free(res);
1806++
1807++ str = "; ; ; ; ;";
1808++ res = _netplan_scrub_systemd_unit_contents(str);
1809++ assert_string_equal(res, "; \\; \\; \\; ;");
1810++ g_free(res);
1811++
1812++ str = " ; ; ; ; ; ";
1813++ res = _netplan_scrub_systemd_unit_contents(str);
1814++ assert_string_equal(res, " \\; \\; \\; \\; \\; ");
1815++ g_free(res);
1816++
1817++ str = "a ; ; ; ; ; b";
1818++ res = _netplan_scrub_systemd_unit_contents(str);
1819++ assert_string_equal(res, "a \\; \\; \\; \\; \\; b");
1820++ g_free(res);
1821++
1822++ str = "\t;\t; ;\t; \t ;\t ";
1823++ res = _netplan_scrub_systemd_unit_contents(str);
1824++ assert_string_equal(res, "\t\\;\t\\; \\;\t\\; \t \\;\t ");
1825++ g_free(res);
1826++
1827++ str = "\t;\t;\t;\t;\t;\t";
1828++ res = _netplan_scrub_systemd_unit_contents(str);
1829++ assert_string_equal(res, "\t\\;\t\\;\t\\;\t\\;\t\\;\t");
1830++ g_free(res);
1831++}
1832++
1833+ int
1834+ setup(__unused void** state)
1835+ {
1836+@@ -380,6 +424,7 @@ main()
1837+ cmocka_unit_test(test_util_is_route_rule_present),
1838+ cmocka_unit_test(test_util_is_string_in_array),
1839+ cmocka_unit_test(test_normalize_ip_address),
1840++ cmocka_unit_test(test_scrub_systemd_unit_content),
1841+ };
1842+
1843+ return cmocka_run_group_tests(tests, setup, tear_down);
1844+diff --git a/tests/generator/test_ovs.py b/tests/generator/test_ovs.py
1845+index 0d27ff9..17c33ed 100644
1846+--- a/tests/generator/test_ovs.py
1847++++ b/tests/generator/test_ovs.py
1848+@@ -1119,3 +1119,61 @@ ExecStart=/usr/bin/ovs-vsctl --may-exist add-br abc/../../123.100 abc/../../123
1849+ ExecStart=/usr/bin/ovs-vsctl set Interface abc/../../123.100 external-ids:netplan=true
1850+ '''},
1851+ 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}})
1852++
1853++ def test_control_characters_and_semicolons_escaping(self):
1854++ self.generate('''network:
1855++ version: 2
1856++ bridges: # bridges first, to trigger multi-pass processing
1857++ ovs0:
1858++ interfaces: [eth0, eth1]
1859++ openvswitch: {}
1860++ ethernets:
1861++ eth0:
1862++ openvswitch:
1863++ external-ids:
1864++ "a\\n1\\ra": " ; a ; 1 ;a; ;b\\t;\\t3 ;\\ta\\t; 1"
1865++ other-config:
1866++ "a\\n1\\ra": " ; a ; 1 ;a; ;b\\t;\\t3 ;\\ta\\t; 1"
1867++ dhcp6: true
1868++ eth1:
1869++ dhcp4: true
1870++ openvswitch:
1871++ other-config:
1872++ disable-in-band: false\n''', skip_generated_yaml_validation=True)
1873++ self.assert_ovs({'ovs0.service': OVS_VIRTUAL % {'iface': 'ovs0', 'extra': '''
1874++[Service]
1875++Type=oneshot
1876++TimeoutStartSec=10s
1877++ExecStart=/usr/bin/ovs-vsctl --may-exist add-br ovs0
1878++ExecStart=/usr/bin/ovs-vsctl --may-exist add-port ovs0 eth1
1879++ExecStart=/usr/bin/ovs-vsctl --may-exist add-port ovs0 eth0
1880++''' + OVS_BR_DEFAULT % {'iface': 'ovs0'}},
1881++ 'eth0.service': OVS_PHYSICAL % {'iface': 'eth0', 'extra': '''\
1882++Requires=netplan-ovs-ovs0.service
1883++After=netplan-ovs-ovs0.service
1884++
1885++[Service]
1886++Type=oneshot
1887++TimeoutStartSec=10s
1888++ExecStart=/usr/bin/ovs-vsctl set Interface eth0 external-ids:a\\n1\\ra= \\; a \\; 1 ;a; ;b\\t;\\t3 ;\\ta\\t; 1
1889++ExecStart=/usr/bin/ovs-vsctl set Interface eth0 external-ids:netplan/external-ids/a\\n1\\ra=,;,a,;,1,;a;,;b\\t;\\t3,;\\ta\\t;,1
1890++ExecStart=/usr/bin/ovs-vsctl set Interface eth0 other-config:a\\n1\\ra= \\; a \\; 1 ;a; ;b\\t;\\t3 ;\\ta\\t; 1
1891++ExecStart=/usr/bin/ovs-vsctl set Interface eth0 external-ids:netplan/other-config/a\\n1\\ra=,;,a,;,1,;a;,;b\\t;\\t3,;\\ta\\t;,1
1892++'''},
1893++ 'eth1.service': OVS_PHYSICAL % {'iface': 'eth1', 'extra': '''\
1894++Requires=netplan-ovs-ovs0.service
1895++After=netplan-ovs-ovs0.service
1896++
1897++[Service]
1898++Type=oneshot
1899++TimeoutStartSec=10s
1900++ExecStart=/usr/bin/ovs-vsctl set Interface eth1 other-config:disable-in-band=false
1901++ExecStart=/usr/bin/ovs-vsctl set Interface eth1 external-ids:netplan/other-config/disable-in-band=false
1902++'''},
1903++ 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}})
1904++ # Confirm that the networkd config is still sane
1905++ self.assert_networkd({'ovs0.network': ND_EMPTY % ('ovs0', 'ipv6'),
1906++ 'eth0.network': (ND_DHCP6 % 'eth0')
1907++ .replace('LinkLocalAddressing=ipv6', 'LinkLocalAddressing=no\nBridge=ovs0'),
1908++ 'eth1.network': (ND_DHCP4 % 'eth1')
1909++ .replace('LinkLocalAddressing=ipv6', 'LinkLocalAddressing=no\nBridge=ovs0')})
1910+diff --git a/tests/test_sriov.py b/tests/test_sriov.py
1911+index 3c06e43..054500c 100644
1912+--- a/tests/test_sriov.py
1913++++ b/tests/test_sriov.py
1914+@@ -913,6 +913,35 @@ After=sys-subsystem-net-devices-engreen.device
1915+ [Service]
1916+ Type=oneshot
1917+ ExecStart=/usr/sbin/netplan rebind enblue engreen
1918++'''})
1919++
1920++ def test_escaping_semicolons_from_unit_file(self):
1921++ ''' Check if semicolons and line breaks are properly escaped in the generated
1922++ systemd service unit.
1923++ '''
1924++ self.generate('''network:
1925++ version: 2
1926++ ethernets:
1927++ engreen:
1928++ embedded-switch-mode: switchdev
1929++ delay-virtual-functions-rebind: true
1930++ enblue:
1931++ match: {driver: dummy_driver}
1932++ set-name: ";en ; a\\t;\\tb ;\\tc\\t; d; \\n;\\nabc"
1933++ embedded-switch-mode: legacy
1934++ delay-virtual-functions-rebind: true
1935++ virtual-function-count: 4
1936++ sriov_vf0:
1937++ link: engreen''', skip_generated_yaml_validation=True)
1938++ self.assert_sriov({'rebind.service': '''[Unit]
1939++Description=(Re-)bind SR-IOV Virtual Functions to their driver
1940++After=network.target
1941++After=sys-subsystem-net-devices-;en \\; a\\t;\\tb ;\\tc\\t; d; \\n;\\nabc.device
1942++After=sys-subsystem-net-devices-engreen.device
1943++
1944++[Service]
1945++Type=oneshot
1946++ExecStart=/usr/sbin/netplan rebind ;en \\; a\\t;\\tb ;\\tc\\t; d; \\n;\\nabc engreen
1947+ '''})
1948+
1949+ def test_rebind_not_delayed(self):
1950diff --git a/debian/patches/series b/debian/patches/series
1951index 3e6ae18..6981f92 100644
1952--- a/debian/patches/series
1953+++ b/debian/patches/series
1954@@ -10,3 +10,7 @@ sru-compat/0011-parse-add-comment-about-OpenFlow16-deprecation.patch
1955 sru-compat/0012-migrate-fix-looback_only-legacy-test.patch
1956 sru-compat/0013-Keep-old-file-permission-for-backwards-compatibility.patch
1957 sru-compat/0014-Demote-lacp-rate-validation-error-to-warning-for-bac.patch
1958+lp2065738/0013-libnetplan-use-more-restrictive-file-permissions.patch
1959+lp2066258/0014-libnetplan-escape-control-characters.patch
1960+lp2066258/0015-backends-escape-file-paths.patch
1961+lp2066258/0016-backends-escape-semicolons-in-service-units.patch

Subscribers

People subscribed via source and target branches