Merge ~danilogondolfo/netplan/+git/ubuntu:jammy_0_107_1_sru_with_security_fixes into ~ubuntu-core-dev/netplan/+git/ubuntu:ubuntu-jammy
- Git
- lp:~danilogondolfo/netplan/+git/ubuntu
- jammy_0_107_1_sru_with_security_fixes
- Merge into 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) |
||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Lukas Märdian | Approve | ||
Ubuntu Core Development Team | Pending | ||
Review via email: mp+468522@code.launchpad.net |
Commit message
Description of the change
A PPA with this change can be found at https:/
Autopkgtest results
The test for s390x failed due to infrastructure problems. I'm retrying it.
Tests are failing due to the known issues related to gre6/vti6 and systemd vs kernel and veths tests is failing due to a known problem in Network Manager.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/debian/changelog b/debian/changelog |
2 | index 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) |
27 | diff --git a/debian/netplan-generator.postinst b/debian/netplan-generator.postinst |
28 | new file mode 100644 |
29 | index 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 | + |
48 | diff --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 |
49 | new file mode 100644 |
50 | index 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 |
489 | diff --git a/debian/patches/lp2066258/0014-libnetplan-escape-control-characters.patch b/debian/patches/lp2066258/0014-libnetplan-escape-control-characters.patch |
490 | new file mode 100644 |
491 | index 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 |
1358 | diff --git a/debian/patches/lp2066258/0015-backends-escape-file-paths.patch b/debian/patches/lp2066258/0015-backends-escape-file-paths.patch |
1359 | new file mode 100644 |
1360 | index 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'}}) |
1652 | diff --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 |
1653 | new file mode 100644 |
1654 | index 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): |
1950 | diff --git a/debian/patches/series b/debian/patches/series |
1951 | index 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 |
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. /github. com/daniloegea/ netplan/ commits/ utf8_strescape/) 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?
- I updated d/changelog to keep this as UNRELEASED for now, pending fixes utf-8 handling in g_strescape() (https:/
- I wonder if we need to reconsider d/p/sru-