Merge ~danilogondolfo/netplan/+git/ubuntu:noble_1.0.1-1ubuntu1 into ~ubuntu-core-dev/netplan/+git/ubuntu:ubuntu-noble

Proposed by Danilo Egea Gondolfo
Status: Merged
Merged at revision: 33e26311cc46ffd78a2e438be62961186dec2c9f
Proposed branch: ~danilogondolfo/netplan/+git/ubuntu:noble_1.0.1-1ubuntu1
Merge into: ~ubuntu-core-dev/netplan/+git/ubuntu:ubuntu-noble
Diff against target: 9520 lines (+2927/-1381)
72 files modified
.github/workflows/autopkgtest.yml (+5/-1)
.github/workflows/build-abi.yml (+1/-0)
.github/workflows/check-address-sanitizer.yml (+1/-0)
.github/workflows/check-coverage.yml (+1/-0)
.github/workflows/coverity.yml (+1/-0)
.github/workflows/debci.yml (+1/-1)
.github/workflows/snapd.patch (+4/-4)
.github/workflows/spread.yml (+5/-1)
README.md (+3/-3)
debian/changelog (+73/-0)
debian/control (+1/-0)
debian/copyright (+1/-1)
debian/patches/0002-CLI-apply-call-udevadm-trigger-using-action-add-Clos.patch (+72/-0)
debian/patches/0003-Revert-wait-online-disabled-wait-online-for-stable-1.patch (+155/-0)
debian/patches/0004-generate-avoid-calling-udevadm-control-reload-LP-199.patch (+82/-0)
debian/patches/series (+3/-16)
debian/tests/control (+1/-1)
dev/null (+0/-386)
doc/.sphinx/requirements.txt (+1/-0)
doc/conf.py (+0/-1)
doc/creating-link-aggregation.md (+86/-0)
doc/howto.md (+26/-14)
doc/matching-interface-by-mac-address.md (+85/-0)
doc/netplan-generate.md (+2/-2)
doc/netplan-status.md (+3/-3)
doc/netplan-tutorial.md (+239/-533)
doc/netplan-yaml.md (+6/-5)
doc/tutorial.md (+2/-0)
doc/using-static-ip-addresses.md (+128/-0)
meson.build (+1/-1)
netplan_cli/__init__.py (+1/-1)
netplan_cli/cli/commands/apply.py (+0/-4)
netplan_cli/cli/commands/generate.py (+10/-1)
netplan_cli/cli/core.py (+2/-2)
netplan_cli/cli/sriov.py (+107/-56)
netplan_cli/cli/state_diff.py (+1/-1)
netplan_cli/cli/utils.py (+1/-1)
python-cffi/netplan/__init__.py (+5/-5)
spread.yaml (+1/-0)
src/generate.c (+25/-10)
src/networkd.c (+219/-58)
src/networkd.h (+5/-0)
src/nm.c (+18/-12)
src/openvswitch.c (+14/-10)
src/parse-nm.c (+15/-0)
src/parse.c (+115/-40)
src/sriov.c (+16/-5)
src/util-internal.h (+12/-0)
src/util.c (+116/-4)
src/validation.c (+4/-3)
src/yaml-helpers.h (+1/-0)
tests/cli/test_get_set.py (+2/-0)
tests/cli_legacy.py (+1/-2)
tests/ctests/meson.build (+1/-0)
tests/ctests/test_netplan_keyfile.c (+65/-0)
tests/ctests/test_netplan_misc.c (+45/-0)
tests/ctests/test_netplan_networkd.c (+74/-0)
tests/ctests/test_netplan_parser.c (+51/-0)
tests/generator/base.py (+3/-0)
tests/generator/test_args.py (+106/-5)
tests/generator/test_auth.py (+11/-11)
tests/generator/test_bonds.py (+14/-0)
tests/generator/test_common.py (+144/-9)
tests/generator/test_ovs.py (+82/-0)
tests/generator/test_wifis.py (+61/-90)
tests/integration/base.py (+93/-0)
tests/integration/diff.py (+2/-2)
tests/integration/ethernets.py (+46/-3)
tests/netplan_dbus/test_dbus.py (+6/-6)
tests/parser/test_keyfile.py (+39/-0)
tests/test_sriov.py (+403/-66)
tests/test_utils.py (+1/-1)
Reviewer Review Type Date Requested Status
Lukas Märdian Approve
Ubuntu Core Development Team Pending
Review via email: mp+470179@code.launchpad.net

Description of the change

This is essentially the same netplan.io found in Oracular.

Apart from the changelog, there is just a small diff with distro specific changes, a patch file I renamed to avoid a prefix number collision and a DEP-3 header I added to another patch file.

A PPA is available here https://launchpad.net/~danilogondolfo/+archive/ubuntu/netplan-sru

Autopkgtests look good except for one "scenarios" test (TestNetworkManager.test_remove_virtual_interfaces) that appears to be flaky only on ppc64el. I executed it several times and it passed once, but failed many times. It *is not* failing on Oracular where netplan.io is basically the same as this package. I tested it with and without the patch that disabled the udev control --reload in the generator and I have the impression that is fails way more often with the patch. It's only happening on ppc64el.

Autopkgtests:

https://autopkgtest.ubuntu.com/results/autopkgtest-noble-danilogondolfo-netplan-sru/noble/s390x/n/netplan.io/20240729_105802_ef836@/log.gz
https://autopkgtest.ubuntu.com/results/autopkgtest-noble-danilogondolfo-netplan-sru/noble/amd64/n/netplan.io/20240729_110739_59501@/log.gz
https://autopkgtest.ubuntu.com/results/autopkgtest-noble-danilogondolfo-netplan-sru/noble/arm64/n/netplan.io/20240729_111042_49323@/log.gz
https://autopkgtest.ubuntu.com/results/autopkgtest-noble-danilogondolfo-netplan-sru/noble/armhf/n/netplan.io/20240729_111154_7f314@/log.gz

On ppc64el, there is one test from "scenarios" (TestNetworkManager.test_remove_virtual_interfaces) that is flaky.

On three consecutive runs it passed once:
https://autopkgtest.ubuntu.com/results/autopkgtest-noble-danilogondolfo-netplan-sru/noble/ppc64el/n/netplan.io/20240729_110320_9209f@/log.gz
https://autopkgtest.ubuntu.com/results/autopkgtest-noble-danilogondolfo-netplan-sru/noble/ppc64el/n/netplan.io/20240729_115407_9dcce@/log.gz
https://autopkgtest.ubuntu.com/results/autopkgtest-noble-danilogondolfo-netplan-sru/noble/ppc64el/n/netplan.io/20240729_115937_363b0@/log.gz

To post a comment you must log in.
Revision history for this message
Lukas Märdian (slyon) wrote :

Thank you very much for running the different autopkgtest from the PPA and doing the analysis. This SRU is well prepared and LGTM!

Some notes from my review (nothing that needs any furhter action):

- note (non-blocking): 0002-CLI-apply-call-udevadm-trigger-using-action-add-Clos.patch Not necessarily needed on Noble's systemd v255, but this SRU is a full (Ubuntu-)version backport, so including this patch is sensible to keep the delta as small as possible.

- note (non-blocking): The SRU is mostly dropping distro patches for https://github.com/canonical/netplan/pull/456 that got included (but disabled) in upstream 1.0.1. This SRU re-enables the functionality, to keep status quo for Noble

- note (non-blocking) Upstream 1.0.1 contains CVE-2022-4968 & related fixes, so we can drop the corresponding distro patches

- note (non-blocking) This SRU matches the upstream 1.0.1 tag, except for the 4 patches in d/p/series.
  - python-limited-stable-api.patch
  - 0002-CLI-apply-call-udevadm-trigger-using-action-add-Clos.patch
  - 0003-Revert-wait-online-disabled-wait-online-for-stable-1.patch
  - 0004-generate-avoid-calling-udevadm-control-reload-LP-199.patch

Merged, tagged & sponsored for SRU review!

review: Approve

Update scan failed

At least one of the branches involved have failed to scan. You can manually schedule a rescan if required.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/doc/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md
2similarity index 100%
3rename from doc/CODE_OF_CONDUCT.md
4rename to .github/CODE_OF_CONDUCT.md
5diff --git a/.github/workflows/autopkgtest.yml b/.github/workflows/autopkgtest.yml
6index 310e99b..5dc8bec 100644
7--- a/.github/workflows/autopkgtest.yml
8+++ b/.github/workflows/autopkgtest.yml
9@@ -33,6 +33,8 @@ jobs:
10 # it's needed (will be auto-loaded) by routing.test_vrf_basic
11 - name: Install dependencies
12 run: |
13+ echo "APT::Get::Always-Include-Phased-Updates \"true\";" | sudo tee /etc/apt/apt.conf.d/90phased-updates
14+ sudo add-apt-repository -y -u -s ppa:slyon/netplan-ci
15 sudo apt update
16 sudo apt install autopkgtest ubuntu-dev-tools devscripts openvswitch-switch linux-modules-extra-$(uname -r)
17 # work around LP: #1878225 as fallback
18@@ -46,7 +48,9 @@ jobs:
19 cp -r netplan.io-*/debian .
20 rm -r debian/patches/ # clear any distro patches
21 # usrmerge-fix
22- sed -i 's|rm debian/tmp/lib/netplan/generate|sh -c "mkdir -p debian/tmp/usr/lib/systemd/system-generators; rm -rf debian/tmp/lib; ln -sf /usr/libexec/netplan/generate debian/tmp/usr/lib/systemd/system-generators/netplan"|' debian/rules
23+ echo "" >> debian/rules
24+ echo "execute_after_dh_auto_install:" >> debian/rules
25+ echo " sh -c \"mkdir -p debian/tmp/usr/lib/systemd/system-generators; rm -rf debian/tmp/lib; ln -sf /usr/libexec/netplan/generate debian/tmp/usr/lib/systemd/system-generators/netplan\"" >> debian/rules
26 # usrmerge-fix-end
27 sed -i 's| systemd-dev|# DELETED: systemd-dev|' debian/control
28 TAG=$(git describe --tags $(git rev-list --tags --max-count=1)) # find latest (stable) tag
29diff --git a/.github/workflows/build-abi.yml b/.github/workflows/build-abi.yml
30index e517101..c7f1b18 100644
31--- a/.github/workflows/build-abi.yml
32+++ b/.github/workflows/build-abi.yml
33@@ -29,6 +29,7 @@ jobs:
34 - name: Install build depends
35 run: |
36 echo "APT::Get::Always-Include-Phased-Updates \"true\";" | sudo tee /etc/apt/apt.conf.d/90phased-updates
37+ sudo add-apt-repository -y -u -s ppa:slyon/netplan-ci
38 sudo apt update
39 sudo apt install abigail-tools ubuntu-dev-tools devscripts equivs
40 pull-lp-source netplan.io
41diff --git a/.github/workflows/check-address-sanitizer.yml b/.github/workflows/check-address-sanitizer.yml
42index 6d97a52..e604a53 100644
43--- a/.github/workflows/check-address-sanitizer.yml
44+++ b/.github/workflows/check-address-sanitizer.yml
45@@ -25,6 +25,7 @@ jobs:
46 - name: Install build depends
47 run: |
48 echo "APT::Get::Always-Include-Phased-Updates \"true\";" | sudo tee /etc/apt/apt.conf.d/90phased-updates
49+ sudo add-apt-repository -y -u -s ppa:slyon/netplan-ci
50 sudo apt update
51 sudo apt -y install curl expect ubuntu-dev-tools devscripts equivs
52 pull-lp-source netplan.io
53diff --git a/.github/workflows/check-coverage.yml b/.github/workflows/check-coverage.yml
54index c8caca7..d18f8b4 100644
55--- a/.github/workflows/check-coverage.yml
56+++ b/.github/workflows/check-coverage.yml
57@@ -26,6 +26,7 @@ jobs:
58 - name: Install build depends
59 run: |
60 echo "APT::Get::Always-Include-Phased-Updates \"true\";" | sudo tee /etc/apt/apt.conf.d/90phased-updates
61+ sudo add-apt-repository -y -u -s ppa:slyon/netplan-ci
62 sudo apt update
63 sudo apt install curl expect ubuntu-dev-tools devscripts equivs gcovr
64 pull-lp-source netplan.io
65diff --git a/.github/workflows/coverity.yml b/.github/workflows/coverity.yml
66index 330652f..f654f30 100644
67--- a/.github/workflows/coverity.yml
68+++ b/.github/workflows/coverity.yml
69@@ -14,6 +14,7 @@ jobs:
70 - name: Install dependencies
71 run: |
72 echo "APT::Get::Always-Include-Phased-Updates \"true\";" | sudo tee /etc/apt/apt.conf.d/90phased-updates
73+ sudo add-apt-repository -y -u -s ppa:slyon/netplan-ci
74 sudo apt update
75 sudo apt -y install curl ubuntu-dev-tools equivs
76 pull-lp-source netplan.io
77diff --git a/.github/workflows/debci.yml b/.github/workflows/debci.yml
78index f20e908..8e9dd5b 100644
79--- a/.github/workflows/debci.yml
80+++ b/.github/workflows/debci.yml
81@@ -45,7 +45,7 @@ jobs:
82 - name: Prepare test
83 run: |
84 # pull-debian-source netplan.io # snapshot.debian.org is not up-to-date
85- V=$(rmadison -u debian -s unstable netplan.io | tail -n1 | cut -d"|" -f2 | xargs)
86+ V=$(rmadison -u debian -s unstable -a source netplan.io | tail -n1 | cut -d"|" -f2 | xargs)
87 dget -u "https://deb.debian.org/debian/pool/main/n/netplan.io/netplan.io_$V.dsc"
88 cp -r netplan.io-*/debian .
89 rm -r debian/patches/ # clear any distro patches
90diff --git a/.github/workflows/snapd.patch b/.github/workflows/snapd.patch
91index 437e1f7..de964c3 100644
92--- a/.github/workflows/snapd.patch
93+++ b/.github/workflows/snapd.patch
94@@ -1,9 +1,9 @@
95 @@ -70,6 +70,8 @@
96
97 sleep 5
98- if lxc exec "$CONTAINER" -- systemctl mask serial-getty@getty.service; then
99-+ lxc exec "$CONTAINER" -- systemctl mask snapd.service
100-+ lxc exec "$CONTAINER" -- systemctl mask snapd.seeded.service
101- lxc exec "$CONTAINER" -- reboot
102+ if "$COMMAND" exec "$CONTAINER" -- systemctl mask serial-getty@getty.service; then
103++ "$COMMAND" exec "$CONTAINER" -- systemctl mask snapd.service
104++ "$COMMAND" exec "$CONTAINER" -- systemctl mask snapd.seeded.service
105+ "$COMMAND" exec "$CONTAINER" -- reboot
106 fi
107
108diff --git a/.github/workflows/spread.yml b/.github/workflows/spread.yml
109index c1593f5..a681057 100644
110--- a/.github/workflows/spread.yml
111+++ b/.github/workflows/spread.yml
112@@ -18,7 +18,11 @@ jobs:
113 - uses: actions/checkout@v3
114 - name: Install spread
115 run: |
116- go install github.com/snapcore/spread/cmd/spread@latest
117+ # TODO: Once Spread PR #179 is merged, go back to:
118+ # go install github.com/snapcore/spread/cmd/spread@latest
119+ git clone --depth 1 --branch thp https://github.com/thp-canonical/spread spread-fork
120+ cd spread-fork && go install ./cmd/spread
121+ cd .. && rm -r spread-fork
122 - name: Run the spread test inside LXD
123 run: |
124 ~/go/bin/spread -v lxd:
125diff --git a/README.md b/README.md
126index 6051ace..68d9e6e 100644
127--- a/README.md
128+++ b/README.md
129@@ -1,4 +1,4 @@
130-# netplan - Backend-agnostic network configuration in YAML
131+# Netplan - Declarative network configuration for various backends
132
133 [![Build+ABI](https://github.com/canonical/netplan/workflows/Build%20&%20ABI%20compatibility/badge.svg?branch=main)](https://github.com/canonical/netplan/actions/workflows/build-abi.yml?query=branch%3Amain)
134 [![Test+Coverage](https://github.com/canonical/netplan/workflows/Unit%20tests%20&%20Coverage/badge.svg?branch=main)](https://github.com/canonical/netplan/actions/workflows/check-coverage.yml?query=branch%3Amain)
135@@ -13,7 +13,7 @@ http://netplan.io
136
137 An overview of the architecture can be found at [netplan.io/design](https://netplan.io/design)
138
139-The full documentation for netplan is available in the [doc/](../main/doc/) directory.
140+Find the full [documentation for Netplan](https://netplan.readthedocs.io) on "Read the Docs".
141
142 # Build using Meson
143
144@@ -30,7 +30,7 @@ Please file bug reports in [Launchpad](https://bugs.launchpad.net/netplan/+fileb
145
146 # Contact us
147
148-Please join us on IRC in #netplan at [Libera.Chat](https://libera.chat/).
149+Please join us on [IRC in #netplan](https://web.libera.chat/gamja/?channels=%23netplan) at Libera.Chat.
150
151 Our mailing list is [here](https://lists.launchpad.net/netplan-developers/).
152
153diff --git a/debian/changelog b/debian/changelog
154index a79d91d..743bab0 100644
155--- a/debian/changelog
156+++ b/debian/changelog
157@@ -1,3 +1,76 @@
158+netplan.io (1.0.1-1ubuntu2~24.04.1) noble; urgency=medium
159+
160+ * Backport netplan.io 1.0.1-1ubuntu2 to 24.04 (LP: #2074197):
161+ - sriov: accept setting the eswitch mode without VFs (LP: 2020409)
162+ - cli/sriov: refactoring
163+ - tests: use proper 0o600 file permissions in more places
164+ - doc: Adding missing 'watchfiles' dependency for Sphinx
165+ - doc: Minor fixes in lang. and mark-up in YAML reference
166+ - doc: Tutorial reorg & lang. + formatting improvements
167+ - networkd: add wait-online enumeration utils
168+ - generate: enable systemd-networkd-wait-online for non-optional interfaces
169+ - CLI:utils: Do not ask for daemon-reload password interactively
170+ - CLI:generate: call daemon-reload after (re-)generating services
171+ - wait-online: Do not block on loopback interface
172+ - generate: Do not touch wait-online, if we don't have any networkd NetDefs
173+ - wait-online: wait for existing interfaces only and downgrade operational
174+ state for interfaces without IP configuration
175+ - wait-online: account for DHCPv4/v6 addresses
176+ - wait-online: do not require virtual devices to be created already
177+ - wait-online: recognize that bridge/bond members will never gain
178+ link-local addresses
179+ - networkd:apply: Drop handling of legacy wpa@ instance units
180+ - wait-online: disabled wait-online for stable 1.0
181+ - test:integration: Try to improve test flakyness
182+ - autopkgtest: More fixes for flaky 'ethernets' test
183+ - Increase some test timeouts to account for slow (riscv64) buildds
184+ SECURITY UPDATE:
185+ - libnetplan: use more restrictive file permissions
186+ (Closes: #1072789, LP: 2065738, LP: 1987842)
187+ - CVE-2022-4968
188+ - libnetplan: escape control characters
189+ - backends: escape file paths
190+ - backends: escape semicolons in service units (LP: 2066258)
191+ Bug fixes:
192+ - cli: Fix logging setup when python-rich is not present
193+ - CI: fix DebCI case for no-change rebuilds
194+ - CI: adopt autopkgtest for 1.0-1 on 22.04
195+ - doc: Update README, move CODE_OF_CONDUCT
196+ - doc: fix en_GB spelling
197+ - CI: adopt snapd.patch for autopkgtest SRU (LP: 2051939)
198+ - parse-nm: add a workaround for the DoT DNS option (LP: 2055148)
199+ - CI: Install netplan-ci PPA
200+ - parse: don't remove datalist items during iteration
201+ - ATTN: parse/bonds: handle same primary in multiple bonds
202+ - parse/bonds: don't fail on primary reassignment
203+ - cli/sriov: set eswitch regardless of pcidev.vfs
204+ - doc: Fix wrong bonds.parameters.mode syntax in example
205+ - parse: fix redefinition of gateway(4|6)
206+ - doc:tutorial: fix whitespace formatting
207+ - util: fix potential NULL pointer assert
208+ - python: elements of __all__ must be strings
209+ - tests: fix diff test with iproute2 6.8
210+ - cli/generate: skip daemon_reload with --mapping
211+ - test: cleanup after wait_online test to fix DebCI
212+ - CI: fork spread to get !179 fixes
213+ - doc: Fix netplan-generate.md formatting !483
214+ - emitter: allow unicode characters in the emitter (LP: 2071652)
215+ - parse: do not escape all non-ascii bytes
216+ * d/t/control: 'diff' autopkgtest is not flaky anymore
217+ * d/patches: Drop patches, applied upstream
218+ * d/p/0002-CLI-apply-call-udevadm-trigger-using-action-add-Clos.patch:
219+ Update 'udevadm trigger' patch, using MOVE action (LP: 2071363)
220+ * d/p/0003-Revert-wait-online-disabled-wait-online-for-stable-1.patch:
221+ Fix wait-online via s-n-wait-online.service.d/10-netplan.
222+ * debian/netplan-generator.postinst: Add a postinst maintainer script to call
223+ the generator, so the file permissions fixes will be applied automatically.
224+ * d/libnetplan1.symbols:
225+ - Update for new internal wait-online symbol
226+ - Update for new (private) symbol
227+ * d/copyright: Update for 2024
228+
229+ -- Danilo Egea Gondolfo <danilo.egea.gondolfo@canonical.com> Wed, 17 Jul 2024 18:12:26 +0100
230+
231 netplan.io (1.0-2ubuntu1.2) noble-security; urgency=medium
232
233 * SECURITY REGRESSION: failure on systems without dbus
234diff --git a/debian/control b/debian/control
235index 86d1d90..fc6bf78 100644
236--- a/debian/control
237+++ b/debian/control
238@@ -27,6 +27,7 @@ Build-Depends:
239 libcmocka-dev,
240 libpython3-dev,
241 libsystemd-dev,
242+ udev <!nocheck>,
243 systemd <!nocheck>,
244 systemd-dev,
245 dbus-x11 <!nocheck>,
246diff --git a/debian/copyright b/debian/copyright
247index f145c74..229f7e4 100644
248--- a/debian/copyright
249+++ b/debian/copyright
250@@ -9,7 +9,7 @@ License: GPL-3
251
252 Files: debian/*
253 Copyright:
254- 2016—2023 Canonical Ltd.
255+ 2016—2024 Canonical Ltd.
256 2018—2023 Andrej Shadura <andrewsh@debian.org>
257 License: GPL-3
258
259diff --git a/debian/patches/0002-CLI-apply-call-udevadm-trigger-using-action-add-Clos.patch b/debian/patches/0002-CLI-apply-call-udevadm-trigger-using-action-add-Clos.patch
260new file mode 100644
261index 0000000..dda68ca
262--- /dev/null
263+++ b/debian/patches/0002-CLI-apply-call-udevadm-trigger-using-action-add-Clos.patch
264@@ -0,0 +1,72 @@
265+From: =?utf-8?q?Lukas_M=C3=A4rdian?= <slyon@ubuntu.com>
266+Date: Wed, 29 May 2024 17:41:28 +0200
267+Subject: CLI:apply: call udevadm trigger,
268+ using --action=add (Closes: #1071220) (LP: #2066344)
269+
270+This will (re)-apply .link files, e.g. to set offloading options
271+
272+Also, containing a drive-by fix, solving FTBFS on unstable (probably /tmp on tmpfs?)
273+
274+Bug: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1071220
275+Bug-Ubuntu: https://pad.lv/2066344
276+Origin: https://github.com/canonical/netplan/pull/479
277+Forwarded: not-needed
278+---
279+ netplan_cli/cli/commands/apply.py | 3 +--
280+ tests/netplan_dbus/test_dbus.py | 8 ++++----
281+ 2 files changed, 5 insertions(+), 5 deletions(-)
282+
283+diff --git a/netplan_cli/cli/commands/apply.py b/netplan_cli/cli/commands/apply.py
284+index 9c88a3c..80c09e7 100644
285+--- a/netplan_cli/cli/commands/apply.py
286++++ b/netplan_cli/cli/commands/apply.py
287+@@ -254,8 +254,7 @@ def command_apply(self, run_generate=True, sync=False, exit_on_error=True, state
288+
289+ # Reloading of udev rules happens during 'netplan generate' already
290+ # subprocess.check_call(['udevadm', 'control', '--reload-rules'])
291+- subprocess.check_call(['udevadm', 'trigger', '--attr-match=subsystem=net'])
292+- subprocess.check_call(['udevadm', 'settle'])
293++ subprocess.check_call(['udevadm', 'trigger', '--action=move', '--subsystem-match=net', '--settle'])
294+
295+ # apply any SR-IOV related changes, if applicable
296+ NetplanApply.process_sriov_config(config_manager, exit_on_error)
297+diff --git a/tests/netplan_dbus/test_dbus.py b/tests/netplan_dbus/test_dbus.py
298+index fdfa92f..9c08af7 100644
299+--- a/tests/netplan_dbus/test_dbus.py
300++++ b/tests/netplan_dbus/test_dbus.py
301+@@ -48,7 +48,7 @@ class TestNetplanDBus(unittest.TestCase):
302+ ethernets:
303+ eth0:
304+ dhcp4: true""")
305+- self.addCleanup(shutil.rmtree, self.tmp)
306++ self.addCleanup(shutil.rmtree, self.tmp, ignore_errors=True)
307+ self.mock_netplan_cmd = MockCmd("netplan")
308+ self._create_mock_system_bus()
309+ self._run_netplan_dbus_on_mock_bus()
310+@@ -284,7 +284,7 @@ class TestNetplanDBus(unittest.TestCase):
311+
312+ cid = self._new_config_object()
313+ tmpdir = self.tmp + '/run/netplan/config-{}'.format(cid)
314+- self.addClassCleanup(shutil.rmtree, tmpdir)
315++ self.addClassCleanup(shutil.rmtree, tmpdir, ignore_errors=True)
316+
317+ # Verify the object path has been created, by calling .Config.Get() on that object
318+ # it would throw an error if it does not exist
319+@@ -319,7 +319,7 @@ class TestNetplanDBus(unittest.TestCase):
320+ def test_netplan_dbus_config_set(self):
321+ cid = self._new_config_object()
322+ tmpdir = self.tmp + '/run/netplan/config-{}'.format(cid)
323+- self.addCleanup(shutil.rmtree, tmpdir)
324++ self.addCleanup(shutil.rmtree, tmpdir, ignore_errors=True)
325+
326+ # Verify .Config.Set() on the config object
327+ # No actual YAML file will be created, as the netplan command is mocked
328+@@ -340,7 +340,7 @@ class TestNetplanDBus(unittest.TestCase):
329+ def test_netplan_dbus_config_get(self):
330+ cid = self._new_config_object()
331+ tmpdir = self.tmp + '/run/netplan/config-{}'.format(cid)
332+- self.addCleanup(shutil.rmtree, tmpdir)
333++ self.addCleanup(shutil.rmtree, tmpdir, ignore_errors=True)
334+
335+ # Verify .Config.Get() on the config object
336+ self.mock_netplan_cmd.set_output("network:\n eth42:\n dhcp6: true")
337diff --git a/debian/patches/0002-parse-nm-add-a-workaround-for-the-DoT-DNS-option.patch b/debian/patches/0002-parse-nm-add-a-workaround-for-the-DoT-DNS-option.patch
338deleted file mode 100644
339index 9732eed..0000000
340--- a/debian/patches/0002-parse-nm-add-a-workaround-for-the-DoT-DNS-option.patch
341+++ /dev/null
342@@ -1,91 +0,0 @@
343-From: Danilo Egea Gondolfo <danilogondolfo@gmail.com>
344-Date: Mon, 25 Mar 2024 11:35:13 +0000
345-Subject: parse-nm: add a workaround for the DoT DNS option
346-
347-Netplan doesn't support DNS options such as SNI. When we parse a
348-nameserver entry with the SNI server name, example: 1.2.3.4#name.domain,
349-the string is added to the list of nameservers without proper
350-parsing/validation.
351-
352-This is a workaround for LP: #2055148.
353-
354-Origin: upstream, https://github.com/canonical/netplan/pull/447
355-Bug-Ubuntu: https://bugs.launchpad.net/ubuntu/+source/netplan.io/+bug/2055148
356----
357- src/parse-nm.c | 15 +++++++++++++++
358- tests/parser/test_keyfile.py | 39 +++++++++++++++++++++++++++++++++++++++
359- 2 files changed, 54 insertions(+)
360-
361-diff --git a/src/parse-nm.c b/src/parse-nm.c
362-index 79275a0..e1b814b 100644
363---- a/src/parse-nm.c
364-+++ b/src/parse-nm.c
365-@@ -348,6 +348,21 @@ parse_nameservers(GKeyFile* kf, const gchar* group, GArray** nameserver_arr)
366- g_assert(nameserver_arr);
367- gchar **split = g_key_file_get_string_list(kf, group, "dns", NULL, NULL);
368- if (split) {
369-+
370-+ /* Workaround for LP: #2055148
371-+ * When an SNI server name is appended to the DNS server IP address,
372-+ * for example: 1.2.3.4#example.com, we skip the parsing and keep the configuration
373-+ * in the passthrough section by checking if the string is a valid IP address.
374-+ * TODO: implement proper DNS options for both NM and ND and drop this
375-+ * workaround.
376-+ */
377-+ for (unsigned i = 0; split[i]; ++i) {
378-+ if (!is_ip4_address(split[i]) && !is_ip6_address(split[i])) {
379-+ g_strfreev(split);
380-+ return;
381-+ }
382-+ }
383-+
384- if (!*nameserver_arr)
385- *nameserver_arr = g_array_new(FALSE, FALSE, sizeof(char*));
386- for(unsigned i = 0; split[i]; ++i) {
387-diff --git a/tests/parser/test_keyfile.py b/tests/parser/test_keyfile.py
388-index 5dcec8e..ce5cdca 100644
389---- a/tests/parser/test_keyfile.py
390-+++ b/tests/parser/test_keyfile.py
391-@@ -2355,3 +2355,42 @@ route1_options=table=1000
392- method=link-local\n'''.format(UUID), expect_fail=True)
393-
394- self.assertIn('missing \'table\' property', out)
395-+
396-+ def test_nameserver_with_DoT_lp2055148(self):
397-+ self.generate_from_keyfile('''[connection]
398-+id=ethernet-eth123
399-+uuid={}
400-+type=ethernet
401-+interface-name=eth123
402-+
403-+[ethernet]
404-+
405-+[ipv4]
406-+dns=8.8.8.8;1.1.1.1#lxd;192.168.0.1#domain.local;
407-+method=auto
408-+
409-+[ipv6]
410-+addr-gen-mode=default
411-+method=auto
412-+
413-+[proxy]\n'''.format(UUID))
414-+ self.assert_netplan({UUID: '''network:
415-+ version: 2
416-+ ethernets:
417-+ NM-{}:
418-+ renderer: NetworkManager
419-+ match:
420-+ name: "eth123"
421-+ dhcp4: true
422-+ dhcp6: true
423-+ wakeonlan: true
424-+ networkmanager:
425-+ uuid: "{}"
426-+ name: "ethernet-eth123"
427-+ passthrough:
428-+ ethernet._: ""
429-+ ipv4.dns: "8.8.8.8;1.1.1.1#lxd;192.168.0.1#domain.local;"
430-+ ipv6.addr-gen-mode: "default"
431-+ ipv6.ip6-privacy: "-1"
432-+ proxy._: ""
433-+'''.format(UUID, UUID)})
434diff --git a/debian/patches/0003-Revert-wait-online-disabled-wait-online-for-stable-1.patch b/debian/patches/0003-Revert-wait-online-disabled-wait-online-for-stable-1.patch
435new file mode 100644
436index 0000000..8fa2663
437--- /dev/null
438+++ b/debian/patches/0003-Revert-wait-online-disabled-wait-online-for-stable-1.patch
439@@ -0,0 +1,155 @@
440+From: =?utf-8?q?Lukas_M=C3=A4rdian?= <slyon@ubuntu.com>
441+Date: Thu, 4 Jul 2024 15:52:19 +0200
442+Subject: Revert "wait-online: disabled wait-online for stable 1.0"
443+
444+This reverts commit 933baebe3f203ac7fd24bb4747cf49afc998784a.
445+---
446+ src/generate.c | 10 +---------
447+ src/networkd.c | 7 +------
448+ tests/generator/test_args.py | 33 +++++++++++++++++++++++++++++++++
449+ tests/integration/ethernets.py | 1 -
450+ 4 files changed, 35 insertions(+), 16 deletions(-)
451+
452+diff --git a/src/generate.c b/src/generate.c
453+index f961126..d6edb0a 100644
454+--- a/src/generate.c
455++++ b/src/generate.c
456+@@ -63,7 +63,7 @@ reload_udevd(void)
457+ * Create enablement symlink for systemd-networkd.service.
458+ */
459+ static void
460+-enable_networkd(const char* generator_dir, __unused gboolean enable_wait_online)
461++enable_networkd(const char* generator_dir, gboolean enable_wait_online)
462+ {
463+ g_autofree char* link = g_build_path(G_DIR_SEPARATOR_S, generator_dir, "multi-user.target.wants", "systemd-networkd.service", NULL);
464+ g_debug("We created networkd configuration, adding %s enablement symlink", link);
465+@@ -75,9 +75,6 @@ enable_networkd(const char* generator_dir, __unused gboolean enable_wait_online)
466+ // LCOV_EXCL_STOP
467+ }
468+
469+- #if 0
470+- // Disabled for netplan 1.0 stable
471+- // LCOV_EXCL_START
472+ if (enable_wait_online) {
473+ g_autofree char* link2 = g_build_path(G_DIR_SEPARATOR_S, generator_dir, "network-online.target.wants", "systemd-networkd-wait-online.service", NULL);
474+ _netplan_safe_mkdir_p_dir(link2);
475+@@ -88,8 +85,6 @@ enable_networkd(const char* generator_dir, __unused gboolean enable_wait_online)
476+ // LCOV_EXCL_STOP
477+ }
478+ }
479+- // LCOV_EXCL_STOP
480+- #endif
481+ }
482+
483+ // LCOV_EXCL_START
484+@@ -335,11 +330,8 @@ int main(int argc, char** argv)
485+ /* covered via 'cloud-init' integration test */
486+ if (any_networkd) {
487+ start_unit_jit("systemd-networkd.socket");
488+- #if 0
489+- // Disabled for netplan 1.0 stable
490+ if (enable_wait_online)
491+ start_unit_jit("systemd-networkd-wait-online.service");
492+- #endif
493+ start_unit_jit("systemd-networkd.service");
494+ }
495+ g_autofree char* glob_run = g_build_path(G_DIR_SEPARATOR_S,
496+diff --git a/src/networkd.c b/src/networkd.c
497+index a6faeb2..b4d7b87 100644
498+--- a/src/networkd.c
499++++ b/src/networkd.c
500+@@ -1551,15 +1551,10 @@ _netplan_networkd_write_wait_online(const NetplanState* np_state, const char* ro
501+ }
502+ g_string_append(content, "\n");
503+
504+- g_string_free(content, TRUE);
505+- #if 0
506+- // Disabled for netplan 1.0 stable
507+- // LCOV_EXCL_START
508+ g_autofree char* new_content = _netplan_scrub_systemd_unit_contents(content->str);
509++ g_string_free(content, TRUE);
510+ content = g_string_new(new_content);
511+ _netplan_g_string_free_to_file_with_permissions(content, rootdir, override, NULL, "root", "root", 0640);
512+- // LCOV_EXCL_STOP
513+- #endif
514+ g_hash_table_destroy(non_optional_interfaces);
515+ return TRUE;
516+ }
517+diff --git a/tests/generator/test_args.py b/tests/generator/test_args.py
518+index 78a76b7..6909194 100644
519+--- a/tests/generator/test_args.py
520++++ b/tests/generator/test_args.py
521+@@ -172,8 +172,21 @@ class TestConfigArgs(TestBase):
522+ os.unlink(n)
523+
524+ # should auto-enable networkd and -wait-online
525++ service_dir = os.path.join(self.workdir.name, 'run', 'systemd', 'system')
526+ self.assertTrue(os.path.islink(os.path.join(
527+ outdir, 'multi-user.target.wants', 'systemd-networkd.service')))
528++ self.assertTrue(os.path.islink(os.path.join(
529++ outdir, 'network-online.target.wants', 'systemd-networkd-wait-online.service')))
530++ override = os.path.join(service_dir, 'systemd-networkd-wait-online.service.d', '10-netplan.conf')
531++ self.assertTrue(os.path.isfile(override))
532++ with open(override, 'r') as f:
533++ # eth99 does not exist on the system, so will not be listed
534++ self.assertEqual(f.read(), '''[Unit]
535++ConditionPathIsSymbolicLink=/run/systemd/generator/network-online.target.wants/systemd-networkd-wait-online.service
536++
537++[Service]
538++ExecStart=
539++ExecStart=/lib/systemd/systemd-networkd-wait-online -i eth99.43:degraded -i br0:degraded -i lo:carrier -i eth99.42:carrier\n''')
540+
541+ # should be a no-op the second time while the stamp exists
542+ out = subprocess.check_output([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir],
543+@@ -210,10 +223,17 @@ class TestConfigArgs(TestBase):
544+ os.unlink(n)
545+
546+ # should auto-enable networkd but not -wait-online
547++ service_dir = os.path.join(self.workdir.name, 'run', 'systemd', 'system')
548+ self.assertTrue(os.path.islink(os.path.join(
549+ outdir, 'multi-user.target.wants', 'systemd-networkd.service')))
550+ self.assertFalse(os.path.islink(os.path.join(
551+ outdir, 'network-online.target.wants', 'systemd-networkd-wait-online.service')))
552++ override = os.path.join(service_dir, 'systemd-networkd-wait-online.service.d', '10-netplan.conf')
553++ self.assertTrue(os.path.isfile(override))
554++ with open(override, 'r') as f:
555++ self.assertEqual(f.read(), '''[Unit]
556++ConditionPathIsSymbolicLink=/run/systemd/generator/network-online.target.wants/systemd-networkd-wait-online.service
557++''')
558+
559+ def test_systemd_generator_noconf(self):
560+ outdir = os.path.join(self.workdir.name, 'out')
561+@@ -269,8 +289,21 @@ class TestConfigArgs(TestBase):
562+ os.unlink(n)
563+
564+ # should auto-enable networkd and -wait-online
565++ service_dir = os.path.join(self.workdir.name, 'run', 'systemd', 'system')
566+ self.assertTrue(os.path.islink(os.path.join(
567+ outdir, 'multi-user.target.wants', 'systemd-networkd.service')))
568++ self.assertTrue(os.path.islink(os.path.join(
569++ outdir, 'network-online.target.wants', 'systemd-networkd-wait-online.service')))
570++ override = os.path.join(service_dir, 'systemd-networkd-wait-online.service.d', '10-netplan.conf')
571++ self.assertTrue(os.path.isfile(override))
572++ with open(override, 'r') as f:
573++ # eth99 does not exist on the system, so will not be listed
574++ self.assertEqual(f.read(), '''[Unit]
575++ConditionPathIsSymbolicLink=/run/systemd/generator/network-online.target.wants/systemd-networkd-wait-online.service
576++
577++[Service]
578++ExecStart=
579++ExecStart=/lib/systemd/systemd-networkd-wait-online -i a \\; b\\t; c\\t; d \\n 123 \\; echo :degraded\n''')
580+
581+ # should be a no-op the second time while the stamp exists
582+ out = subprocess.check_output([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir],
583+diff --git a/tests/integration/ethernets.py b/tests/integration/ethernets.py
584+index c42e8b5..4ac7fe7 100644
585+--- a/tests/integration/ethernets.py
586++++ b/tests/integration/ethernets.py
587+@@ -391,7 +391,6 @@ class TestNetworkd(IntegrationTestsBase, _CommonTests):
588+ ['inet6 9876:bbbb::11/70', 'inet 172.16.5.3/20'],
589+ ['inet6 fe80:', 'inet 169.254.'])
590+
591+- @unittest.skip("networkd wait-online is disabled for netplan 1.0 stable")
592+ def test_systemd_networkd_wait_online(self):
593+ self.addCleanup(subprocess.call, ['ip', 'link', 'del', 'br0'])
594+ self.setup_eth(None, False)
595diff --git a/debian/patches/0004-generate-avoid-calling-udevadm-control-reload-LP-199.patch b/debian/patches/0004-generate-avoid-calling-udevadm-control-reload-LP-199.patch
596new file mode 100644
597index 0000000..8e5bb87
598--- /dev/null
599+++ b/debian/patches/0004-generate-avoid-calling-udevadm-control-reload-LP-199.patch
600@@ -0,0 +1,82 @@
601+From: =?utf-8?q?Lukas_M=C3=A4rdian?= <slyon@ubuntu.com>
602+Date: Thu, 4 Jul 2024 12:50:53 +0200
603+Subject: generate: avoid calling 'udevadm control --reload' (LP: #1999178)
604+
605+The udev rules directories are monitored and re-loaded automatically with
606+modern systemd-udevd. No need to manually reload them in the generator,
607+causing side-effects.
608+
609+We can still force-reload them when issuing 'netplan generate' or
610+'netplan apply' through the CLI, just to be on the safe side.
611+
612+Replaces: https://github.com/canonical/netplan/pull/304
613+
614+Bug-Ubuntu: https://bugs.launchpad.net/netplan/+bug/1999178
615+Origin: https://github.com/canonical/netplan/pull/488
616+---
617+ netplan_cli/cli/commands/apply.py | 3 +--
618+ netplan_cli/cli/commands/generate.py | 4 ++++
619+ src/generate.c | 14 --------------
620+ 3 files changed, 5 insertions(+), 16 deletions(-)
621+
622+diff --git a/netplan_cli/cli/commands/apply.py b/netplan_cli/cli/commands/apply.py
623+index cfcaf26..a4e744f 100644
624+--- a/netplan_cli/cli/commands/apply.py
625++++ b/netplan_cli/cli/commands/apply.py
626+@@ -252,8 +252,7 @@ class NetplanApply(utils.NetplanCommand):
627+ stdout=subprocess.DEVNULL,
628+ stderr=subprocess.DEVNULL)
629+
630+- # Reloading of udev rules happens during 'netplan generate' already
631+- # subprocess.check_call(['udevadm', 'control', '--reload-rules'])
632++ subprocess.check_call(['udevadm', 'control', '--reload'])
633+ subprocess.check_call(['udevadm', 'trigger', '--action=move', '--subsystem-match=net', '--settle'])
634+
635+ # apply any SR-IOV related changes, if applicable
636+diff --git a/netplan_cli/cli/commands/generate.py b/netplan_cli/cli/commands/generate.py
637+index ba51eb0..889411d 100644
638+--- a/netplan_cli/cli/commands/generate.py
639++++ b/netplan_cli/cli/commands/generate.py
640+@@ -82,6 +82,10 @@ class NetplanGenerate(utils.NetplanCommand):
641+ argv += ['--mapping', self.mapping]
642+ logging.debug('command generate: running %s', argv)
643+ res = subprocess.call(argv)
644++ try:
645++ subprocess.check_call(['udevadm', 'control', '--reload'])
646++ except subprocess.CalledProcessError as e:
647++ logging.debug(f'Could not call "udevadm control --reload": {str(e)}')
648+ # reload systemd, as we might have changed service units, such as
649+ # /run/systemd/system/systemd-networkd-wait-online.service.d/10-netplan.conf
650+ # Skip it if --mapping is used as nothing will be generated
651+diff --git a/src/generate.c b/src/generate.c
652+index d6edb0a..ba02a32 100644
653+--- a/src/generate.c
654++++ b/src/generate.c
655+@@ -52,13 +52,6 @@ static GOptionEntry options[] = {
656+ {NULL}
657+ };
658+
659+-static void
660+-reload_udevd(void)
661+-{
662+- const gchar *argv[] = { "/bin/udevadm", "control", "--reload", NULL };
663+- g_spawn_sync(NULL, (gchar**)argv, NULL, G_SPAWN_STDERR_TO_DEV_NULL, NULL, NULL, NULL, NULL, NULL, NULL);
664+-};
665+-
666+ /**
667+ * Create enablement symlink for systemd-networkd.service.
668+ */
669+@@ -288,13 +281,6 @@ int main(int argc, char** argv)
670+
671+ CHECK_CALL(netplan_state_finish_nm_write(np_state, rootdir, &error));
672+ CHECK_CALL(netplan_state_finish_sriov_write(np_state, rootdir, &error));
673+- /* We may have written .rules & .link files, thus we must
674+- * invalidate udevd cache of its config as by default it only
675+- * invalidates cache at most every 3 seconds. Not sure if this
676+- * should live in `generate' or `apply', but it is confusing
677+- * when udevd ignores just-in-time created rules files.
678+- */
679+- reload_udevd();
680+ }
681+
682+ /* Disable /usr/lib/NetworkManager/conf.d/10-globally-managed-devices.conf
683diff --git a/debian/patches/lp2060311/0003-util-fix-potential-NULL-pointer-assert.patch b/debian/patches/lp2060311/0003-util-fix-potential-NULL-pointer-assert.patch
684deleted file mode 100644
685index 9110a71..0000000
686--- a/debian/patches/lp2060311/0003-util-fix-potential-NULL-pointer-assert.patch
687+++ /dev/null
688@@ -1,24 +0,0 @@
689-From: =?utf-8?q?Lukas_M=C3=A4rdian?= <slyon@ubuntu.com>
690-Date: Tue, 16 Apr 2024 09:50:51 +0200
691-Subject: util: fix potential NULL pointer assert
692-
693-Origin: https://github.com/canonical/netplan/pull/456/commits/e56a3dcedbf82d5154ad9cc87ef27a375fdfb9bb
694-Bug-Ubuntu: https://bugs.launchpad.net/netplan/+bug/2060311
695-Forwarded: https://github.com/canonical/netplan/pull/456
696----
697- src/util.c | 2 +-
698- 1 file changed, 1 insertion(+), 1 deletion(-)
699-
700-diff --git a/src/util.c b/src/util.c
701-index 36eb896..3e6c80d 100644
702---- a/src/util.c
703-+++ b/src/util.c
704-@@ -955,7 +955,7 @@ netplan_netdef_match_interface(const NetplanNetDefinition* netdef, const char* n
705- return !g_strcmp0(name, netdef->id);
706-
707- if (netdef->match.mac) {
708-- if (g_ascii_strcasecmp(netdef->match.mac, mac))
709-+ if (g_ascii_strcasecmp(netdef->match.mac ?: "", mac ?: ""))
710- return FALSE;
711- }
712-
713diff --git a/debian/patches/lp2060311/0004-networkd-add-wait-online-enumeration-utils.patch b/debian/patches/lp2060311/0004-networkd-add-wait-online-enumeration-utils.patch
714deleted file mode 100644
715index d842be6..0000000
716--- a/debian/patches/lp2060311/0004-networkd-add-wait-online-enumeration-utils.patch
717+++ /dev/null
718@@ -1,189 +0,0 @@
719-From: =?utf-8?q?Lukas_M=C3=A4rdian?= <slyon@ubuntu.com>
720-Date: Tue, 16 Apr 2024 09:30:34 +0200
721-Subject: networkd: add wait-online enumeration utils
722-
723-Origin: https://github.com/canonical/netplan/pull/456/commits/4bc25c9ae24b6a6a7e81b92ae901cb7fef4c30df
724-Bug-Ubuntu: https://bugs.launchpad.net/netplan/+bug/2060311
725-Forwarded: https://github.com/canonical/netplan/pull/456
726----
727- src/networkd.c | 65 +++++++++++++++++++++++++++++++
728- tests/ctests/meson.build | 1 +
729- tests/ctests/test_netplan_networkd.c | 74 ++++++++++++++++++++++++++++++++++++
730- 3 files changed, 140 insertions(+)
731- create mode 100644 tests/ctests/test_netplan_networkd.c
732-
733-diff --git a/src/networkd.c b/src/networkd.c
734-index 25121c4..9289388 100644
735---- a/src/networkd.c
736-+++ b/src/networkd.c
737-@@ -20,6 +20,7 @@
738- #include <unistd.h>
739- #include <ctype.h>
740- #include <errno.h>
741-+#include <net/if.h>
742- #include <sys/stat.h>
743-
744- #include <glib.h>
745-@@ -32,6 +33,70 @@
746- #include "util-internal.h"
747- #include "validation.h"
748-
749-+/**
750-+ * Query sysfs for the MAC address (up to 20 bytes for infiniband) of @ifname
751-+ * The caller owns the returned string and needs to free it.
752-+ */
753-+STATIC char*
754-+_netplan_sysfs_get_mac_by_ifname(const char* ifname, const char* rootdir)
755-+{
756-+ g_autofree gchar* content = NULL;
757-+ g_autofree gchar* sysfs_path = NULL;
758-+ sysfs_path = g_build_path(G_DIR_SEPARATOR_S, rootdir ?: G_DIR_SEPARATOR_S,
759-+ "sys", "class", "net", ifname, "address", NULL);
760-+
761-+ if (!g_file_get_contents (sysfs_path, &content, NULL, NULL)) {
762-+ g_debug("%s: Cannot read file contents.", __FUNCTION__);
763-+ return NULL;
764-+ }
765-+
766-+ // Trim whitespace & clone value
767-+ return g_strdup(g_strstrip(content));
768-+}
769-+
770-+/**
771-+ * Query sysfs for the driver used by @ifname
772-+ * The caller owns the returned string and needs to free it.
773-+ */
774-+STATIC char*
775-+_netplan_sysfs_get_driver_by_ifname(const char* ifname, const char* rootdir)
776-+{
777-+ g_autofree gchar* link = NULL;
778-+ g_autofree gchar* sysfs_path = NULL;
779-+ sysfs_path = g_build_path(G_DIR_SEPARATOR_S, rootdir ?: G_DIR_SEPARATOR_S,
780-+ "sys", "class", "net", ifname, "device", "driver", NULL);
781-+
782-+ link = g_file_read_link(sysfs_path, NULL);
783-+ if (!link) {
784-+ g_debug("%s: Cannot read symlink of %s.", __FUNCTION__, sysfs_path);
785-+ return NULL;
786-+ }
787-+
788-+ return g_path_get_basename(link);
789-+}
790-+
791-+/**
792-+ * Enumerate all network interfaces (/sys/clas/net/...) and check
793-+ * netplan_netdef_match_interface() to see if they match the current NetDef
794-+ */
795-+STATIC void
796-+_netplan_enumerate_interfaces(const NetplanNetDefinition* def, GHashTable* tbl, const char* rootdir)
797-+{
798-+ g_assert(tbl);
799-+ struct if_nameindex *if_nidxs, *intf;
800-+ if_nidxs = if_nameindex();
801-+ if (if_nidxs != NULL) {
802-+ for (intf = if_nidxs; intf->if_index != 0 || intf->if_name != NULL; intf++) {
803-+ if (g_hash_table_contains(tbl, intf->if_name)) continue;
804-+ g_autofree gchar* mac = _netplan_sysfs_get_mac_by_ifname(intf->if_name, rootdir);
805-+ g_autofree gchar* driver = _netplan_sysfs_get_driver_by_ifname(intf->if_name, rootdir);
806-+ if (netplan_netdef_match_interface(def, intf->if_name, mac, driver))
807-+ g_hash_table_add(tbl, g_strdup(intf->if_name));
808-+ }
809-+ if_freenameindex(if_nidxs);
810-+ }
811-+}
812-+
813- /**
814- * Append WiFi frequencies to wpa_supplicant's freq_list=
815- */
816-diff --git a/tests/ctests/meson.build b/tests/ctests/meson.build
817-index ed3bc5d..dcf4b6c 100644
818---- a/tests/ctests/meson.build
819-+++ b/tests/ctests/meson.build
820-@@ -5,6 +5,7 @@ tests = {
821- 'test_netplan_misc': false,
822- 'test_netplan_validation': false,
823- 'test_netplan_keyfile': false,
824-+ 'test_netplan_networkd': false,
825- 'test_netplan_nm': false,
826- 'test_netplan_openvswitch': false,
827- }
828-diff --git a/tests/ctests/test_netplan_networkd.c b/tests/ctests/test_netplan_networkd.c
829-new file mode 100644
830-index 0000000..e9e4b79
831---- /dev/null
832-+++ b/tests/ctests/test_netplan_networkd.c
833-@@ -0,0 +1,74 @@
834-+#include <stdio.h>
835-+#include <stdarg.h>
836-+#include <stddef.h>
837-+#include <setjmp.h>
838-+
839-+#include <cmocka.h>
840-+
841-+#include "netplan.h"
842-+#include "util-internal.h"
843-+#include "networkd.c"
844-+
845-+#include "test_utils.h"
846-+
847-+void
848-+test_wait_online_utils(__unused void** state)
849-+{
850-+ char template[] = "/tmp/netplan.XXXXXX";
851-+ const char* rootdir = mkdtemp(template);
852-+
853-+ // create mock sysfs
854-+ g_autofree gchar* sys = g_strdup_printf("%s/sys", rootdir);
855-+ g_autofree gchar* sys_class = g_strdup_printf("%s/sys/class", rootdir);
856-+ g_autofree gchar* sys_class_net = g_strdup_printf("%s/sys/class/net", rootdir);
857-+ g_autofree gchar* eth99 = g_strdup_printf("%s/sys/class/net/eth99", rootdir);
858-+ g_autofree gchar* eth99_device = g_strdup_printf("%s/device", eth99);
859-+ g_autofree gchar* driver = g_strdup_printf("%s/device/driver", eth99);
860-+ g_autofree gchar* mac = g_strdup_printf("%s/address", eth99);
861-+ g_mkdir_with_parents(eth99_device, 0700);
862-+
863-+ // assert MAC address file
864-+ assert_true(g_file_set_contents(mac, " aa:bb:cc:dd:ee:ff \r\n\n", -1, NULL));
865-+ g_autofree gchar* mac_value = _netplan_sysfs_get_mac_by_ifname("eth99", rootdir);
866-+ assert_string_equal(mac_value, "aa:bb:cc:dd:ee:ff");
867-+
868-+ // assert driver link
869-+ assert_int_equal(symlink("../somewhere/drivers/mock_drv", driver), 0);
870-+ g_autofree gchar* driver_value = _netplan_sysfs_get_driver_by_ifname("eth99", rootdir);
871-+ assert_string_equal(driver_value, "mock_drv");
872-+
873-+ // Cleanup
874-+ remove(mac);
875-+ remove(driver);
876-+ rmdir(eth99_device);
877-+ rmdir(eth99);
878-+ rmdir(sys_class_net);
879-+ rmdir(sys_class);
880-+ rmdir(sys);
881-+ rmdir(rootdir);
882-+}
883-+
884-+
885-+int
886-+setup(__unused void** state)
887-+{
888-+ return 0;
889-+}
890-+
891-+int
892-+tear_down(__unused void** state)
893-+{
894-+ return 0;
895-+}
896-+
897-+int
898-+main()
899-+{
900-+
901-+ const struct CMUnitTest tests[] = {
902-+ cmocka_unit_test(test_wait_online_utils),
903-+ };
904-+
905-+ return cmocka_run_group_tests(tests, setup, tear_down);
906-+
907-+}
908diff --git a/debian/patches/lp2060311/0005-generate-enable-systemd-networkd-wait-online-for-non.patch b/debian/patches/lp2060311/0005-generate-enable-systemd-networkd-wait-online-for-non.patch
909deleted file mode 100644
910index 573d344..0000000
911--- a/debian/patches/lp2060311/0005-generate-enable-systemd-networkd-wait-online-for-non.patch
912+++ /dev/null
913@@ -1,332 +0,0 @@
914-From: =?utf-8?q?Lukas_M=C3=A4rdian?= <slyon@ubuntu.com>
915-Date: Tue, 16 Apr 2024 09:51:40 +0200
916-Subject: generate: enable systemd-networkd-wait-online for non-optional
917- interfaces only
918-
919-Skip s-n-wait-online if we don't have any non-optional interfaces, using a
920-"ConditionPathIsSymbolicLink=" checking Netplan's s-n-wait-online.service
921-enablement symlink.
922-
923-This is in favor to RequiredForOnline=yes as the behavior of upstream (pure)
924-systemd-networkd-wait-online.service is not mean to be used in this way.
925-
926-If "RequiredForOnline=no" sd-networkd-wait-online will fully ignore the
927-corresponding interface and it will block/delay network-online.target if
928-no interfaces are "RequiredForOnline=yes" at all.
929-
930-FR-7246
931-
932-Origin: https://github.com/canonical/netplan/pull/456/commits/494b2e41dd9e17b79502aa53b49fd92e82acf5c1
933-Bug-Ubuntu: https://bugs.launchpad.net/netplan/+bug/2060311
934-Forwarded: https://github.com/canonical/netplan/pull/456
935----
936- src/generate.c | 24 ++++++++-------
937- src/networkd.c | 66 +++++++++++++++++++++++++++++++++++-------
938- src/networkd.h | 3 ++
939- tests/generator/base.py | 2 ++
940- tests/generator/test_args.py | 56 +++++++++++++++++++++++++++++++++++
941- tests/generator/test_common.py | 5 ----
942- 6 files changed, 131 insertions(+), 25 deletions(-)
943-
944-diff --git a/src/generate.c b/src/generate.c
945-index 35179a2..19198ff 100644
946---- a/src/generate.c
947-+++ b/src/generate.c
948-@@ -63,7 +63,7 @@ reload_udevd(void)
949- * Create enablement symlink for systemd-networkd.service.
950- */
951- static void
952--enable_networkd(const char* generator_dir)
953-+enable_networkd(const char* generator_dir, gboolean enable_wait_online)
954- {
955- g_autofree char* link = g_build_path(G_DIR_SEPARATOR_S, generator_dir, "multi-user.target.wants", "systemd-networkd.service", NULL);
956- g_debug("We created networkd configuration, adding %s enablement symlink", link);
957-@@ -75,13 +75,15 @@ enable_networkd(const char* generator_dir)
958- // LCOV_EXCL_STOP
959- }
960-
961-- g_autofree char* link2 = g_build_path(G_DIR_SEPARATOR_S, generator_dir, "network-online.target.wants", "systemd-networkd-wait-online.service", NULL);
962-- _netplan_safe_mkdir_p_dir(link2);
963-- if (symlink("/lib/systemd/system/systemd-networkd-wait-online.service", link2) < 0 && errno != EEXIST) {
964-- // LCOV_EXCL_START
965-- g_fprintf(stderr, "failed to create enablement symlink: %m\n");
966-- exit(1);
967-- // LCOV_EXCL_STOP
968-+ if (enable_wait_online) {
969-+ g_autofree char* link2 = g_build_path(G_DIR_SEPARATOR_S, generator_dir, "network-online.target.wants", "systemd-networkd-wait-online.service", NULL);
970-+ _netplan_safe_mkdir_p_dir(link2);
971-+ if (symlink("/lib/systemd/system/systemd-networkd-wait-online.service", link2) < 0 && errno != EEXIST) {
972-+ // LCOV_EXCL_START
973-+ g_fprintf(stderr, "failed to create enablement symlink: %m\n");
974-+ exit(1);
975-+ // LCOV_EXCL_STOP
976-+ }
977- }
978- }
979-
980-@@ -300,10 +302,11 @@ int main(int argc, char** argv)
981- if (netplan_state_get_backend(np_state) == NETPLAN_BACKEND_NM || any_nm)
982- _netplan_g_string_free_to_file(g_string_new(NULL), rootdir, "/run/NetworkManager/conf.d/10-globally-managed-devices.conf", NULL);
983-
984-+ gboolean enable_wait_online = _netplan_networkd_write_wait_online(np_state, rootdir);
985- if (called_as_generator) {
986- /* Ensure networkd starts if we have any configuration for it */
987- if (any_networkd)
988-- enable_networkd(files[0]);
989-+ enable_networkd(files[0], enable_wait_online);
990-
991- /* Leave a stamp file so that we don't regenerate the configuration
992- * multiple times and userspace can wait for it to finish */
993-@@ -324,7 +327,8 @@ int main(int argc, char** argv)
994- /* covered via 'cloud-init' integration test */
995- if (any_networkd) {
996- start_unit_jit("systemd-networkd.socket");
997-- start_unit_jit("systemd-networkd-wait-online.service");
998-+ if (enable_wait_online)
999-+ start_unit_jit("systemd-networkd-wait-online.service");
1000- start_unit_jit("systemd-networkd.service");
1001- }
1002- g_autofree char* glob_run = g_build_path(G_DIR_SEPARATOR_S,
1003-diff --git a/src/networkd.c b/src/networkd.c
1004-index 9289388..cd1f7a2 100644
1005---- a/src/networkd.c
1006-+++ b/src/networkd.c
1007-@@ -803,7 +803,6 @@ _netplan_netdef_write_network_file(
1008- g_autoptr(GString) link = NULL;
1009- GString* s = NULL;
1010- mode_t orig_umask;
1011-- gboolean is_optional = def->optional;
1012-
1013- SET_OPT_OUT_PTR(has_been_written, FALSE);
1014-
1015-@@ -826,17 +825,9 @@ _netplan_netdef_write_network_file(
1016- else /* "off" */
1017- mode = "always-down";
1018- g_string_append_printf(link, "ActivationPolicy=%s\n", mode);
1019-- /* When activation-mode is used we default to being optional.
1020-- * Otherwise systemd might wait indefinitely for the interface to
1021-- * become online.
1022-- */
1023-- is_optional = TRUE;
1024- }
1025-
1026-- if (is_optional || def->optional_addresses) {
1027-- if (is_optional) {
1028-- g_string_append(link, "RequiredForOnline=no\n");
1029-- }
1030-+ if (def->optional_addresses) {
1031- for (unsigned i = 0; NETPLAN_OPTIONAL_ADDRESS_TYPES[i].name != NULL; ++i) {
1032- if (def->optional_addresses & NETPLAN_OPTIONAL_ADDRESS_TYPES[i].flag) {
1033- g_string_append_printf(link, "OptionalAddresses=%s\n", NETPLAN_OPTIONAL_ADDRESS_TYPES[i].name);
1034-@@ -1466,6 +1457,60 @@ _netplan_netdef_write_networkd(
1035- return TRUE;
1036- }
1037-
1038-+gboolean
1039-+_netplan_networkd_write_wait_online(const NetplanState* np_state, const char* rootdir)
1040-+{
1041-+ // Hash set of non-optional interfaces to wait for
1042-+ GHashTable* non_optional_interfaces = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
1043-+ NetplanStateIterator iter;
1044-+ netplan_state_iterator_init(np_state, &iter);
1045-+ while (netplan_state_iterator_has_next(&iter)) {
1046-+ const NetplanNetDefinition* def = netplan_state_iterator_next(&iter);
1047-+ if (def->backend != NETPLAN_BACKEND_NETWORKD)
1048-+ continue;
1049-+ /* When activation-mode is used we default to being optional.
1050-+ * Otherwise, systemd might wait indefinitely for the interface to
1051-+ * come online.
1052-+ */
1053-+ if (!(def->optional || def->activation_mode)) {
1054-+ if (!netplan_netdef_has_match(def)) { // no matching => single interface
1055-+ g_hash_table_add(non_optional_interfaces, g_strdup(def->id));
1056-+ } else if (def->set_name) { // matching on a single interface
1057-+ g_hash_table_add(non_optional_interfaces, g_strdup(def->set_name));
1058-+ } else { // matching on potentially multiple interfaces
1059-+ // XXX: we shouldn't run this enumeration for every NetDef...
1060-+ _netplan_enumerate_interfaces(def, non_optional_interfaces, rootdir);
1061-+ }
1062-+ }
1063-+ }
1064-+
1065-+ // create run/systemd/system/systemd-networkd-wait-online.service.d/
1066-+ const char* override = "/run/systemd/system/systemd-networkd-wait-online.service.d/10-netplan.conf";
1067-+ // The "ConditionPathIsSymbolicLink" is Netplan's s-n-wait-online enablement symlink,
1068-+ // as we want to run -wait-online only if enabled by Netplan.
1069-+ GString* content = g_string_new("[Unit]\n"
1070-+ "ConditionPathIsSymbolicLink=/run/systemd/generator/network-online.target.wants/systemd-networkd-wait-online.service\n");
1071-+ if (g_hash_table_size(non_optional_interfaces) == 0) {
1072-+ _netplan_g_string_free_to_file(content, rootdir, override, NULL);
1073-+ g_hash_table_destroy(non_optional_interfaces);
1074-+ return FALSE;
1075-+ }
1076-+
1077-+ // We have non-optional interface, so let's wait for those explicitly
1078-+ GHashTableIter idx;
1079-+ gpointer ifname;
1080-+ g_string_append(content, "\n[Service]\nExecStart=\n"
1081-+ "ExecStart=/lib/systemd/systemd-networkd-wait-online");
1082-+ g_hash_table_iter_init (&idx, non_optional_interfaces);
1083-+ while (g_hash_table_iter_next (&idx, &ifname, NULL))
1084-+ g_string_append_printf(content, " -i %s", (const char*)ifname);
1085-+ g_string_append(content, "\n");
1086-+
1087-+ _netplan_g_string_free_to_file(content, rootdir, override, NULL);
1088-+ g_hash_table_destroy(non_optional_interfaces);
1089-+ return TRUE;
1090-+}
1091-+
1092- /**
1093- * Clean up all generated configurations in @rootdir from previous runs.
1094- */
1095-@@ -1479,6 +1524,7 @@ _netplan_networkd_cleanup(const char* rootdir)
1096- _netplan_unlink_glob(rootdir, "/run/udev/rules.d/99-netplan-*");
1097- _netplan_unlink_glob(rootdir, "/run/systemd/system/network.target.wants/netplan-regdom.service");
1098- _netplan_unlink_glob(rootdir, "/run/systemd/system/netplan-regdom.service");
1099-+ _netplan_unlink_glob(rootdir, "/run/systemd/system/systemd-networkd-wait-online.service.d/10-netplan*.conf");
1100- /* Historically (up to v0.98) we had netplan-wpa@*.service files, in case of an
1101- * upgraded system, we need to make sure to clean those up. */
1102- _netplan_unlink_glob(rootdir, "/run/systemd/system/systemd-networkd.service.wants/netplan-wpa@*.service");
1103-diff --git a/src/networkd.h b/src/networkd.h
1104-index 2bd0848..8332b7b 100644
1105---- a/src/networkd.h
1106-+++ b/src/networkd.h
1107-@@ -37,5 +37,8 @@ _netplan_netdef_write_network_file(
1108- gboolean* has_been_written,
1109- GError** error);
1110-
1111-+NETPLAN_INTERNAL gboolean
1112-+_netplan_networkd_write_wait_online(const NetplanState* np_state, const char* rootdir);
1113-+
1114- NETPLAN_INTERNAL void
1115- _netplan_networkd_cleanup(const char* rootdir);
1116-diff --git a/tests/generator/base.py b/tests/generator/base.py
1117-index bcc9d8a..4f27e94 100644
1118---- a/tests/generator/base.py
1119-+++ b/tests/generator/base.py
1120-@@ -465,6 +465,8 @@ class TestBase(unittest.TestCase):
1121- self.assertEqual(set(os.listdir(self.workdir.name)) - {'lib'}, {'etc', 'run'})
1122- ovs_systemd_dir = set(os.listdir(systemd_dir))
1123- ovs_systemd_dir.remove('systemd-networkd.service.wants')
1124-+ if 'systemd-networkd-wait-online.service.d' in ovs_systemd_dir:
1125-+ ovs_systemd_dir.remove('systemd-networkd-wait-online.service.d')
1126- self.assertEqual(ovs_systemd_dir, {'netplan-ovs-' + f for f in file_contents_map})
1127- for fname, contents in file_contents_map.items():
1128- fname = 'netplan-ovs-' + fname
1129-diff --git a/tests/generator/test_args.py b/tests/generator/test_args.py
1130-index 2c5ffe2..ffa603a 100644
1131---- a/tests/generator/test_args.py
1132-+++ b/tests/generator/test_args.py
1133-@@ -124,6 +124,11 @@ class TestConfigArgs(TestBase):
1134- version: 2
1135- ethernets:
1136- eth0:
1137-+ dhcp4: true
1138-+ eth1:
1139-+ dhcp4: true
1140-+ optional: true
1141-+ eth2:
1142- dhcp4: true''')
1143- outdir = os.path.join(self.workdir.name, 'out')
1144- os.mkdir(outdir)
1145-@@ -136,12 +141,28 @@ class TestConfigArgs(TestBase):
1146- n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-eth0.network')
1147- self.assertTrue(os.path.exists(n))
1148- os.unlink(n)
1149-+ n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-eth1.network')
1150-+ self.assertTrue(os.path.exists(n))
1151-+ os.unlink(n)
1152-+ n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-eth2.network')
1153-+ self.assertTrue(os.path.exists(n))
1154-+ os.unlink(n)
1155-
1156- # should auto-enable networkd and -wait-online
1157-+ service_dir = os.path.join(self.workdir.name, 'run', 'systemd', 'system')
1158- self.assertTrue(os.path.islink(os.path.join(
1159- outdir, 'multi-user.target.wants', 'systemd-networkd.service')))
1160- self.assertTrue(os.path.islink(os.path.join(
1161- outdir, 'network-online.target.wants', 'systemd-networkd-wait-online.service')))
1162-+ override = os.path.join(service_dir, 'systemd-networkd-wait-online.service.d', '10-netplan.conf')
1163-+ self.assertTrue(os.path.isfile(override))
1164-+ with open(override, 'r') as f:
1165-+ self.assertEqual(f.read(), '''[Unit]
1166-+ConditionPathIsSymbolicLink=/run/systemd/generator/network-online.target.wants/systemd-networkd-wait-online.service
1167-+
1168-+[Service]
1169-+ExecStart=
1170-+ExecStart=/lib/systemd/systemd-networkd-wait-online -i eth0 -i eth2\n''')
1171-
1172- # should be a no-op the second time while the stamp exists
1173- out = subprocess.check_output([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir],
1174-@@ -155,6 +176,41 @@ class TestConfigArgs(TestBase):
1175- subprocess.check_output([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir])
1176- self.assertTrue(os.path.exists(n))
1177-
1178-+ def test_systemd_generator_all_optional(self):
1179-+ conf = os.path.join(self.confdir, 'a.yaml')
1180-+ os.makedirs(os.path.dirname(conf))
1181-+ with open(conf, 'w') as f:
1182-+ f.write('''network:
1183-+ version: 2
1184-+ ethernets:
1185-+ eth0:
1186-+ dhcp4: true
1187-+ optional: true''')
1188-+ outdir = os.path.join(self.workdir.name, 'out')
1189-+ os.mkdir(outdir)
1190-+
1191-+ generator = os.path.join(self.workdir.name, 'systemd', 'system-generators', 'netplan')
1192-+ os.makedirs(os.path.dirname(generator))
1193-+ os.symlink(exe_generate, generator)
1194-+
1195-+ subprocess.check_call([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir])
1196-+ n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-eth0.network')
1197-+ self.assertTrue(os.path.exists(n))
1198-+ os.unlink(n)
1199-+
1200-+ # should auto-enable networkd but not -wait-online
1201-+ service_dir = os.path.join(self.workdir.name, 'run', 'systemd', 'system')
1202-+ self.assertTrue(os.path.islink(os.path.join(
1203-+ outdir, 'multi-user.target.wants', 'systemd-networkd.service')))
1204-+ self.assertFalse(os.path.islink(os.path.join(
1205-+ outdir, 'network-online.target.wants', 'systemd-networkd-wait-online.service')))
1206-+ override = os.path.join(service_dir, 'systemd-networkd-wait-online.service.d', '10-netplan.conf')
1207-+ self.assertTrue(os.path.isfile(override))
1208-+ with open(override, 'r') as f:
1209-+ self.assertEqual(f.read(), '''[Unit]
1210-+ConditionPathIsSymbolicLink=/run/systemd/generator/network-online.target.wants/systemd-networkd-wait-online.service
1211-+''')
1212-+
1213- def test_systemd_generator_noconf(self):
1214- outdir = os.path.join(self.workdir.name, 'out')
1215- os.mkdir(outdir)
1216-diff --git a/tests/generator/test_common.py b/tests/generator/test_common.py
1217-index 785f6f4..d98eb32 100644
1218---- a/tests/generator/test_common.py
1219-+++ b/tests/generator/test_common.py
1220-@@ -35,9 +35,6 @@ class TestNetworkd(TestBase):
1221- self.assert_networkd({'eth0.network': '''[Match]
1222- Name=eth0
1223-
1224--[Link]
1225--RequiredForOnline=no
1226--
1227- [Network]
1228- DHCP=ipv6
1229- LinkLocalAddressing=ipv6
1230-@@ -85,7 +82,6 @@ Name=eth0
1231-
1232- [Link]
1233- ActivationPolicy=always-down
1234--RequiredForOnline=no
1235-
1236- [Network]
1237- DHCP=ipv6
1238-@@ -109,7 +105,6 @@ Name=eth0
1239-
1240- [Link]
1241- ActivationPolicy=manual
1242--RequiredForOnline=no
1243-
1244- [Network]
1245- DHCP=ipv6
1246diff --git a/debian/patches/lp2060311/0006-CLI-utils-Do-not-ask-for-daemon-reload-password-inte.patch b/debian/patches/lp2060311/0006-CLI-utils-Do-not-ask-for-daemon-reload-password-inte.patch
1247deleted file mode 100644
1248index b05d01a..0000000
1249--- a/debian/patches/lp2060311/0006-CLI-utils-Do-not-ask-for-daemon-reload-password-inte.patch
1250+++ /dev/null
1251@@ -1,53 +0,0 @@
1252-From: =?utf-8?q?Lukas_M=C3=A4rdian?= <slyon@ubuntu.com>
1253-Date: Wed, 17 Apr 2024 10:23:16 +0200
1254-Subject: CLI:utils: Do not ask for daemon-reload password interactively
1255-
1256-Origin: https://github.com/canonical/netplan/pull/456/commits/03c269d172955bff0fedff9504f6fa80e5a0b413
1257-Bug-Ubuntu: https://bugs.launchpad.net/netplan/+bug/2060311
1258-Forwarded: https://github.com/canonical/netplan/pull/456
1259----
1260- netplan_cli/cli/utils.py | 2 +-
1261- tests/cli_legacy.py | 3 +--
1262- tests/test_utils.py | 2 +-
1263- 3 files changed, 3 insertions(+), 4 deletions(-)
1264-
1265-diff --git a/netplan_cli/cli/utils.py b/netplan_cli/cli/utils.py
1266-index c89af1a..c9f9ce2 100644
1267---- a/netplan_cli/cli/utils.py
1268-+++ b/netplan_cli/cli/utils.py
1269-@@ -165,7 +165,7 @@ def systemctl_is_installed(unit_pattern):
1270-
1271- def systemctl_daemon_reload():
1272- '''Reload systemd unit files from disk and re-calculate its dependencies'''
1273-- subprocess.check_call(['systemctl', 'daemon-reload'])
1274-+ subprocess.check_call(['systemctl', 'daemon-reload', '--no-ask-password'])
1275-
1276-
1277- def ip_addr_flush(iface):
1278-diff --git a/tests/cli_legacy.py b/tests/cli_legacy.py
1279-index 897043e..decde81 100755
1280---- a/tests/cli_legacy.py
1281-+++ b/tests/cli_legacy.py
1282-@@ -98,8 +98,7 @@ class TestGenerate(unittest.TestCase):
1283- enlol: {dhcp4: yes}''')
1284- os.chmod(path_a, mode=0o600)
1285- os.chmod(path_b, mode=0o600)
1286-- out = subprocess.check_output(exe_cli + ['generate', '--root-dir', self.workdir.name], stderr=subprocess.STDOUT)
1287-- self.assertEqual(out, b'')
1288-+ subprocess.check_call(exe_cli + ['generate', '--root-dir', self.workdir.name])
1289- self.assertEqual(os.listdir(os.path.join(self.workdir.name, 'run', 'systemd', 'network')),
1290- ['10-netplan-enlol.network'])
1291-
1292-diff --git a/tests/test_utils.py b/tests/test_utils.py
1293-index 55c907c..c7ab060 100644
1294---- a/tests/test_utils.py
1295-+++ b/tests/test_utils.py
1296-@@ -380,7 +380,7 @@ class TestUtils(unittest.TestCase):
1297- os.environ['PATH'] = os.path.dirname(self.mock_cmd.path) + os.pathsep + path_env
1298- utils.systemctl_daemon_reload()
1299- self.assertEqual(self.mock_cmd.calls(), [
1300-- ['systemctl', 'daemon-reload']
1301-+ ['systemctl', 'daemon-reload', '--no-ask-password']
1302- ])
1303-
1304- def test_ip_addr_flush(self):
1305diff --git a/debian/patches/lp2060311/0007-CLI-generate-call-daemon-reload-after-re-generating-.patch b/debian/patches/lp2060311/0007-CLI-generate-call-daemon-reload-after-re-generating-.patch
1306deleted file mode 100644
1307index 2998726..0000000
1308--- a/debian/patches/lp2060311/0007-CLI-generate-call-daemon-reload-after-re-generating-.patch
1309+++ /dev/null
1310@@ -1,29 +0,0 @@
1311-From: =?utf-8?q?Lukas_M=C3=A4rdian?= <slyon@ubuntu.com>
1312-Date: Wed, 17 Apr 2024 10:23:51 +0200
1313-Subject: CLI:generate: call daemon-reload after (re-)generating services
1314-
1315-Origin: https://github.com/canonical/netplan/pull/456/commits/f3d9994ae3d45d5e114e40efe6040319caaf5850
1316-Bug-Ubuntu: https://bugs.launchpad.net/netplan/+bug/2060311
1317-Forwarded: https://github.com/canonical/netplan/pull/456
1318----
1319- netplan_cli/cli/commands/generate.py | 9 ++++++++-
1320- 1 file changed, 8 insertions(+), 1 deletion(-)
1321-
1322-diff --git a/netplan_cli/cli/commands/generate.py b/netplan_cli/cli/commands/generate.py
1323-index f84b171..dcbbb53 100644
1324---- a/netplan_cli/cli/commands/generate.py
1325-+++ b/netplan_cli/cli/commands/generate.py
1326-@@ -81,5 +81,12 @@ class NetplanGenerate(utils.NetplanCommand):
1327- if self.mapping:
1328- argv += ['--mapping', self.mapping]
1329- logging.debug('command generate: running %s', argv)
1330-+ res = subprocess.call(argv)
1331-+ # reload systemd, as we might have changed service units, such as
1332-+ # /run/systemd/system/systemd-networkd-wait-online.service.d/10-netplan.conf
1333-+ try:
1334-+ utils.systemctl_daemon_reload()
1335-+ except subprocess.CalledProcessError as e:
1336-+ logging.warning(e)
1337- # FIXME: os.execv(argv[0], argv) would be better but fails coverage
1338-- sys.exit(subprocess.call(argv))
1339-+ sys.exit(res)
1340diff --git a/debian/patches/lp2060311/0008-wait-online-Do-not-block-on-loopback-interface.patch b/debian/patches/lp2060311/0008-wait-online-Do-not-block-on-loopback-interface.patch
1341deleted file mode 100644
1342index 34c3ec2..0000000
1343--- a/debian/patches/lp2060311/0008-wait-online-Do-not-block-on-loopback-interface.patch
1344+++ /dev/null
1345@@ -1,71 +0,0 @@
1346-From: =?utf-8?q?Lukas_M=C3=A4rdian?= <slyon@ubuntu.com>
1347-Date: Wed, 17 Apr 2024 10:24:36 +0200
1348-Subject: wait-online: Do not block on loopback interface
1349-
1350-Origin: https://github.com/canonical/netplan/pull/456/commits/bd9456999aa6a1a71a475985c8b8556f57ed2b2f
1351-Bug-Ubuntu: https://bugs.launchpad.net/netplan/+bug/2060311
1352-Forwarded: https://github.com/canonical/netplan/pull/456
1353----
1354- src/networkd.c | 12 +++++++++---
1355- tests/generator/test_args.py | 8 ++++----
1356- 2 files changed, 13 insertions(+), 7 deletions(-)
1357-
1358-diff --git a/src/networkd.c b/src/networkd.c
1359-index cd1f7a2..862176b 100644
1360---- a/src/networkd.c
1361-+++ b/src/networkd.c
1362-@@ -1498,12 +1498,18 @@ _netplan_networkd_write_wait_online(const NetplanState* np_state, const char* ro
1363-
1364- // We have non-optional interface, so let's wait for those explicitly
1365- GHashTableIter idx;
1366-- gpointer ifname;
1367-+ gpointer key;
1368- g_string_append(content, "\n[Service]\nExecStart=\n"
1369- "ExecStart=/lib/systemd/systemd-networkd-wait-online");
1370- g_hash_table_iter_init (&idx, non_optional_interfaces);
1371-- while (g_hash_table_iter_next (&idx, &ifname, NULL))
1372-- g_string_append_printf(content, " -i %s", (const char*)ifname);
1373-+ while (g_hash_table_iter_next (&idx, &key, NULL)) {
1374-+ const char* ifname = key;
1375-+ g_string_append_printf(content, " -i %s", ifname);
1376-+ // XXX: We should be checking IFF_LOOPBACK instead of interface name.
1377-+ // But don't have access to the flags here.
1378-+ if (!g_strcmp0(ifname, "lo"))
1379-+ g_string_append(content, ":carrier"); // "carrier" as min-oper state for loopback
1380-+ }
1381- g_string_append(content, "\n");
1382-
1383- _netplan_g_string_free_to_file(content, rootdir, override, NULL);
1384-diff --git a/tests/generator/test_args.py b/tests/generator/test_args.py
1385-index ffa603a..0db4d38 100644
1386---- a/tests/generator/test_args.py
1387-+++ b/tests/generator/test_args.py
1388-@@ -128,8 +128,8 @@ class TestConfigArgs(TestBase):
1389- eth1:
1390- dhcp4: true
1391- optional: true
1392-- eth2:
1393-- dhcp4: true''')
1394-+ lo:
1395-+ addresses: ["127.0.0.1/8", "::1/128"]''')
1396- outdir = os.path.join(self.workdir.name, 'out')
1397- os.mkdir(outdir)
1398-
1399-@@ -144,7 +144,7 @@ class TestConfigArgs(TestBase):
1400- n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-eth1.network')
1401- self.assertTrue(os.path.exists(n))
1402- os.unlink(n)
1403-- n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-eth2.network')
1404-+ n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-lo.network')
1405- self.assertTrue(os.path.exists(n))
1406- os.unlink(n)
1407-
1408-@@ -162,7 +162,7 @@ ConditionPathIsSymbolicLink=/run/systemd/generator/network-online.target.wants/s
1409-
1410- [Service]
1411- ExecStart=
1412--ExecStart=/lib/systemd/systemd-networkd-wait-online -i eth0 -i eth2\n''')
1413-+ExecStart=/lib/systemd/systemd-networkd-wait-online -i lo:carrier -i eth0\n''')
1414-
1415- # should be a no-op the second time while the stamp exists
1416- out = subprocess.check_output([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir],
1417diff --git a/debian/patches/lp2060311/0009-generate-Do-not-touch-wait-online-if-we-don-t-have-a.patch b/debian/patches/lp2060311/0009-generate-Do-not-touch-wait-online-if-we-don-t-have-a.patch
1418deleted file mode 100644
1419index a836418..0000000
1420--- a/debian/patches/lp2060311/0009-generate-Do-not-touch-wait-online-if-we-don-t-have-a.patch
1421+++ /dev/null
1422@@ -1,44 +0,0 @@
1423-From: =?utf-8?q?Lukas_M=C3=A4rdian?= <slyon@ubuntu.com>
1424-Date: Wed, 17 Apr 2024 10:27:27 +0200
1425-Subject: generate: Do not touch wait-online,
1426- if we don't have any networkd NetDefs
1427-
1428-Origin: https://github.com/canonical/netplan/pull/456/commits/e98eac8074cdd7cd99d17a40b15ae414b9eb6d3d
1429-Bug-Ubuntu: https://bugs.launchpad.net/netplan/+bug/2060311
1430-Forwarded: https://github.com/canonical/netplan/pull/456
1431----
1432- src/generate.c | 5 ++++-
1433- tests/generator/test_args.py | 4 ++++
1434- 2 files changed, 8 insertions(+), 1 deletion(-)
1435-
1436-diff --git a/src/generate.c b/src/generate.c
1437-index 19198ff..d6edb0a 100644
1438---- a/src/generate.c
1439-+++ b/src/generate.c
1440-@@ -302,7 +302,10 @@ int main(int argc, char** argv)
1441- if (netplan_state_get_backend(np_state) == NETPLAN_BACKEND_NM || any_nm)
1442- _netplan_g_string_free_to_file(g_string_new(NULL), rootdir, "/run/NetworkManager/conf.d/10-globally-managed-devices.conf", NULL);
1443-
1444-- gboolean enable_wait_online = _netplan_networkd_write_wait_online(np_state, rootdir);
1445-+ gboolean enable_wait_online = FALSE;
1446-+ if (any_networkd)
1447-+ enable_wait_online = _netplan_networkd_write_wait_online(np_state, rootdir);
1448-+
1449- if (called_as_generator) {
1450- /* Ensure networkd starts if we have any configuration for it */
1451- if (any_networkd)
1452-diff --git a/tests/generator/test_args.py b/tests/generator/test_args.py
1453-index 0db4d38..5e36cf2 100644
1454---- a/tests/generator/test_args.py
1455-+++ b/tests/generator/test_args.py
1456-@@ -39,6 +39,10 @@ class TestConfigArgs(TestBase):
1457- self.assert_nm(None)
1458- self.assert_nm_udev(None)
1459- self.assert_ovs({'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}})
1460-+ # should not touch -wait-online
1461-+ service_dir = os.path.join(self.workdir.name, 'run', 'systemd', 'system')
1462-+ override = os.path.join(service_dir, 'systemd-networkd-wait-online.service.d', '10-netplan.conf')
1463-+ self.assertFalse(os.path.isfile(override))
1464-
1465- def test_empty_config(self):
1466- self.generate('')
1467diff --git a/debian/patches/lp2060311/0010-wait-online-wait-for-existing-interfaces-only-and-do.patch b/debian/patches/lp2060311/0010-wait-online-wait-for-existing-interfaces-only-and-do.patch
1468deleted file mode 100644
1469index 8568aa5..0000000
1470--- a/debian/patches/lp2060311/0010-wait-online-wait-for-existing-interfaces-only-and-do.patch
1471+++ /dev/null
1472@@ -1,238 +0,0 @@
1473-From: =?utf-8?q?Lukas_M=C3=A4rdian?= <slyon@ubuntu.com>
1474-Date: Wed, 17 Apr 2024 12:01:13 +0200
1475-Subject: wait-online: wait for existing interfaces only and downgrade
1476- operational state for interfaces without IP configuration
1477-
1478-Origin: https://github.com/canonical/netplan/pull/456/commits/98d7f8af2013abc47dbaf3f11913ca06fb741255
1479-Bug-Ubuntu: https://bugs.launchpad.net/netplan/+bug/2060311
1480-Forwarded: https://github.com/canonical/netplan/pull/456
1481----
1482- src/networkd.c | 71 +++++++++++++++++++++++++++++-------------
1483- tests/generator/test_args.py | 10 +++---
1484- tests/integration/ethernets.py | 37 ++++++++++++++++++++++
1485- 3 files changed, 92 insertions(+), 26 deletions(-)
1486-
1487-diff --git a/src/networkd.c b/src/networkd.c
1488-index 862176b..71432d4 100644
1489---- a/src/networkd.c
1490-+++ b/src/networkd.c
1491-@@ -75,28 +75,42 @@ _netplan_sysfs_get_driver_by_ifname(const char* ifname, const char* rootdir)
1492- return g_path_get_basename(link);
1493- }
1494-
1495--/**
1496-- * Enumerate all network interfaces (/sys/clas/net/...) and check
1497-- * netplan_netdef_match_interface() to see if they match the current NetDef
1498-- */
1499- STATIC void
1500--_netplan_enumerate_interfaces(const NetplanNetDefinition* def, GHashTable* tbl, const char* rootdir)
1501-+_netplan_query_system_interfaces(GHashTable* tbl)
1502- {
1503- g_assert(tbl);
1504- struct if_nameindex *if_nidxs, *intf;
1505- if_nidxs = if_nameindex();
1506- if (if_nidxs != NULL) {
1507-- for (intf = if_nidxs; intf->if_index != 0 || intf->if_name != NULL; intf++) {
1508-- if (g_hash_table_contains(tbl, intf->if_name)) continue;
1509-- g_autofree gchar* mac = _netplan_sysfs_get_mac_by_ifname(intf->if_name, rootdir);
1510-- g_autofree gchar* driver = _netplan_sysfs_get_driver_by_ifname(intf->if_name, rootdir);
1511-- if (netplan_netdef_match_interface(def, intf->if_name, mac, driver))
1512-- g_hash_table_add(tbl, g_strdup(intf->if_name));
1513-- }
1514-+ for (intf = if_nidxs; intf->if_index != 0 || intf->if_name != NULL; intf++)
1515-+ g_hash_table_add(tbl, g_strdup(intf->if_name));
1516- if_freenameindex(if_nidxs);
1517- }
1518- }
1519-
1520-+/**
1521-+ * Enumerate all network interfaces (/sys/clas/net/...) and check
1522-+ * netplan_netdef_match_interface() to see if they match the current NetDef
1523-+ */
1524-+STATIC void
1525-+_netplan_enumerate_interfaces(const NetplanNetDefinition* def, GHashTable* ifaces, GHashTable* tbl, const char* carrier, const char* set_name, const char* rootdir)
1526-+{
1527-+ g_assert(ifaces);
1528-+ g_assert(tbl);
1529-+
1530-+ GHashTableIter iter;
1531-+ gpointer key;
1532-+ g_hash_table_iter_init (&iter, ifaces);
1533-+ while (g_hash_table_iter_next (&iter, &key, NULL)) {
1534-+ const char* ifname = key;
1535-+ if (g_hash_table_contains(tbl, ifname)|| (set_name && g_hash_table_contains(tbl, set_name))) continue;
1536-+ g_autofree gchar* mac = _netplan_sysfs_get_mac_by_ifname(ifname, rootdir);
1537-+ g_autofree gchar* driver = _netplan_sysfs_get_driver_by_ifname(ifname, rootdir);
1538-+ if (netplan_netdef_match_interface(def, ifname, mac, driver))
1539-+ g_hash_table_replace(tbl, set_name ? g_strdup(set_name) : g_strdup(ifname), g_strdup(carrier));
1540-+ }
1541-+}
1542-+
1543- /**
1544- * Append WiFi frequencies to wpa_supplicant's freq_list=
1545- */
1546-@@ -1460,29 +1474,41 @@ _netplan_netdef_write_networkd(
1547- gboolean
1548- _netplan_networkd_write_wait_online(const NetplanState* np_state, const char* rootdir)
1549- {
1550-+ // Set of all current network interfaces, potentially non yet renamed
1551-+ GHashTable* system_interfaces = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
1552-+ _netplan_query_system_interfaces(system_interfaces);
1553-+
1554- // Hash set of non-optional interfaces to wait for
1555-- GHashTable* non_optional_interfaces = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
1556-+ GHashTable* non_optional_interfaces = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
1557- NetplanStateIterator iter;
1558- netplan_state_iterator_init(np_state, &iter);
1559- while (netplan_state_iterator_has_next(&iter)) {
1560-- const NetplanNetDefinition* def = netplan_state_iterator_next(&iter);
1561-+ NetplanNetDefinition* def = netplan_state_iterator_next(&iter);
1562- if (def->backend != NETPLAN_BACKEND_NETWORKD)
1563- continue;
1564-+
1565- /* When activation-mode is used we default to being optional.
1566- * Otherwise, systemd might wait indefinitely for the interface to
1567- * come online.
1568- */
1569- if (!(def->optional || def->activation_mode)) {
1570-- if (!netplan_netdef_has_match(def)) { // no matching => single interface
1571-- g_hash_table_add(non_optional_interfaces, g_strdup(def->id));
1572-- } else if (def->set_name) { // matching on a single interface
1573-- g_hash_table_add(non_optional_interfaces, g_strdup(def->set_name));
1574-+ // Check if we have any IP configuration
1575-+ struct address_iter* addr_iter = _netplan_netdef_new_address_iter(def);
1576-+ gboolean any_ips = _netplan_address_iter_next(addr_iter) != NULL || netplan_netdef_get_link_local_ipv4(def) || netplan_netdef_get_link_local_ipv6(def);
1577-+ _netplan_address_iter_free(addr_iter);
1578-+
1579-+ // no matching => single interface, ignoring non-existing interfaces
1580-+ if (!netplan_netdef_has_match(def) && g_hash_table_contains(system_interfaces, def->id)) {
1581-+ g_hash_table_replace(non_optional_interfaces, g_strdup(def->id), any_ips ? g_strdup("degraded") : g_strdup("carrier"));
1582-+ } else if (def->set_name) { // matching on a single interface, to be renamed
1583-+ _netplan_enumerate_interfaces(def, system_interfaces, non_optional_interfaces, any_ips ? "degraded" : "carrier", def->set_name, rootdir);
1584- } else { // matching on potentially multiple interfaces
1585- // XXX: we shouldn't run this enumeration for every NetDef...
1586-- _netplan_enumerate_interfaces(def, non_optional_interfaces, rootdir);
1587-+ _netplan_enumerate_interfaces(def, system_interfaces, non_optional_interfaces, any_ips ? "degraded" : "carrier", NULL, rootdir);
1588- }
1589- }
1590- }
1591-+ g_hash_table_destroy(system_interfaces);
1592-
1593- // create run/systemd/system/systemd-networkd-wait-online.service.d/
1594- const char* override = "/run/systemd/system/systemd-networkd-wait-online.service.d/10-netplan.conf";
1595-@@ -1498,17 +1524,20 @@ _netplan_networkd_write_wait_online(const NetplanState* np_state, const char* ro
1596-
1597- // We have non-optional interface, so let's wait for those explicitly
1598- GHashTableIter idx;
1599-- gpointer key;
1600-+ gpointer key, value;
1601- g_string_append(content, "\n[Service]\nExecStart=\n"
1602- "ExecStart=/lib/systemd/systemd-networkd-wait-online");
1603- g_hash_table_iter_init (&idx, non_optional_interfaces);
1604-- while (g_hash_table_iter_next (&idx, &key, NULL)) {
1605-+ while (g_hash_table_iter_next (&idx, &key, &value)) {
1606- const char* ifname = key;
1607-+ const char* min_oper_state = value;
1608- g_string_append_printf(content, " -i %s", ifname);
1609- // XXX: We should be checking IFF_LOOPBACK instead of interface name.
1610- // But don't have access to the flags here.
1611- if (!g_strcmp0(ifname, "lo"))
1612- g_string_append(content, ":carrier"); // "carrier" as min-oper state for loopback
1613-+ else if (min_oper_state)
1614-+ g_string_append_printf(content, ":%s", min_oper_state);
1615- }
1616- g_string_append(content, "\n");
1617-
1618-diff --git a/tests/generator/test_args.py b/tests/generator/test_args.py
1619-index 5e36cf2..de5aaa8 100644
1620---- a/tests/generator/test_args.py
1621-+++ b/tests/generator/test_args.py
1622-@@ -127,9 +127,9 @@ class TestConfigArgs(TestBase):
1623- f.write('''network:
1624- version: 2
1625- ethernets:
1626-- eth0:
1627-+ eth99:
1628- dhcp4: true
1629-- eth1:
1630-+ eth98:
1631- dhcp4: true
1632- optional: true
1633- lo:
1634-@@ -142,10 +142,10 @@ class TestConfigArgs(TestBase):
1635- os.symlink(exe_generate, generator)
1636-
1637- subprocess.check_call([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir])
1638-- n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-eth0.network')
1639-+ n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-eth99.network')
1640- self.assertTrue(os.path.exists(n))
1641- os.unlink(n)
1642-- n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-eth1.network')
1643-+ n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-eth98.network')
1644- self.assertTrue(os.path.exists(n))
1645- os.unlink(n)
1646- n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-lo.network')
1647-@@ -166,7 +166,7 @@ ConditionPathIsSymbolicLink=/run/systemd/generator/network-online.target.wants/s
1648-
1649- [Service]
1650- ExecStart=
1651--ExecStart=/lib/systemd/systemd-networkd-wait-online -i lo:carrier -i eth0\n''')
1652-+ExecStart=/lib/systemd/systemd-networkd-wait-online -i lo:carrier\n''') # eth99 does not exist on the system
1653-
1654- # should be a no-op the second time while the stamp exists
1655- out = subprocess.check_output([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir],
1656-diff --git a/tests/integration/ethernets.py b/tests/integration/ethernets.py
1657-index e43f533..7028a49 100644
1658---- a/tests/integration/ethernets.py
1659-+++ b/tests/integration/ethernets.py
1660-@@ -22,6 +22,7 @@
1661- # You should have received a copy of the GNU General Public License
1662- # along with this program. If not, see <http://www.gnu.org/licenses/>.
1663-
1664-+import os
1665- import sys
1666- import subprocess
1667- import unittest
1668-@@ -390,6 +391,42 @@ class TestNetworkd(IntegrationTestsBase, _CommonTests):
1669- ['inet6 9876:bbbb::11/70', 'inet 172.16.5.3/20'],
1670- ['inet6 fe80:', 'inet 169.254.'])
1671-
1672-+ def test_systemd_networkd_wait_online(self):
1673-+ self.setup_eth(None)
1674-+ with open(self.config, 'w') as f:
1675-+ f.write('''network:
1676-+ renderer: %(r)s
1677-+ ethernets:
1678-+ lo:
1679-+ addresses: ["127.0.0.1/8", "::1/128"]
1680-+ optional: true
1681-+ doesnotexist:
1682-+ addresses: ["10.0.0.42/24"]
1683-+ findme:
1684-+ match:
1685-+ macaddress: %(ec_mac)s
1686-+ link-local: []
1687-+ set-name: "findme"
1688-+ %(e2c)s:
1689-+ addresses: ["10.0.0.1/24"]
1690-+''' % {'r': self.backend, 'ec_mac': self.dev_e_client_mac, 'e2c': self.dev_e2_client})
1691-+ self.generate_and_settle([self.dev_e2_client])
1692-+ override = os.path.join('/run', 'systemd', 'system', 'systemd-networkd-wait-online.service.d', '10-netplan.conf')
1693-+ self.assertTrue(os.path.isfile(override))
1694-+
1695-+ with open(override, 'r') as f:
1696-+ # lo is optional/ignored and should not be listed
1697-+ # doesnotexist should not be listed, as it does not exist
1698-+ # <dev_e_client> should be listed as "findme", using reduced operational state
1699-+ # <dev_e2_client> should be listed normally
1700-+ self.assertEqual(f.read(), '''[Unit]
1701-+ConditionPathIsSymbolicLink=/run/systemd/generator/network-online.target.wants/systemd-networkd-wait-online.service
1702-+
1703-+[Service]
1704-+ExecStart=
1705-+ExecStart=/lib/systemd/systemd-networkd-wait-online -i %s:degraded -i findme:carrier
1706-+''' % self.dev_e2_client)
1707-+
1708-
1709- @unittest.skipIf("NetworkManager" not in test_backends,
1710- "skipping as NetworkManager backend tests are disabled")
1711diff --git a/debian/patches/lp2060311/0011-wait-online-account-for-DHCPv4-v6-addresses.patch b/debian/patches/lp2060311/0011-wait-online-account-for-DHCPv4-v6-addresses.patch
1712deleted file mode 100644
1713index 954ee57..0000000
1714--- a/debian/patches/lp2060311/0011-wait-online-account-for-DHCPv4-v6-addresses.patch
1715+++ /dev/null
1716@@ -1,28 +0,0 @@
1717-From: =?utf-8?q?Lukas_M=C3=A4rdian?= <slyon@ubuntu.com>
1718-Date: Wed, 17 Apr 2024 17:42:27 +0200
1719-Subject: wait-online: account for DHCPv4/v6 addresses
1720-
1721-Origin: https://github.com/canonical/netplan/pull/456/commits/3d62dfbf3bbd028e4e9d69f7f1a5ddd803d60a33
1722-Bug-Ubuntu: https://bugs.launchpad.net/netplan/+bug/2060311
1723-Forwarded: https://github.com/canonical/netplan/pull/456
1724----
1725- src/networkd.c | 6 +++++-
1726- 1 file changed, 5 insertions(+), 1 deletion(-)
1727-
1728-diff --git a/src/networkd.c b/src/networkd.c
1729-index 71432d4..f4c081c 100644
1730---- a/src/networkd.c
1731-+++ b/src/networkd.c
1732-@@ -1494,7 +1494,11 @@ _netplan_networkd_write_wait_online(const NetplanState* np_state, const char* ro
1733- if (!(def->optional || def->activation_mode)) {
1734- // Check if we have any IP configuration
1735- struct address_iter* addr_iter = _netplan_netdef_new_address_iter(def);
1736-- gboolean any_ips = _netplan_address_iter_next(addr_iter) != NULL || netplan_netdef_get_link_local_ipv4(def) || netplan_netdef_get_link_local_ipv6(def);
1737-+ gboolean any_ips = _netplan_address_iter_next(addr_iter) != NULL
1738-+ || netplan_netdef_get_dhcp4(def)
1739-+ || netplan_netdef_get_dhcp6(def)
1740-+ || netplan_netdef_get_link_local_ipv4(def)
1741-+ || netplan_netdef_get_link_local_ipv6(def);
1742- _netplan_address_iter_free(addr_iter);
1743-
1744- // no matching => single interface, ignoring non-existing interfaces
1745diff --git a/debian/patches/lp2060311/0012-wait-online-do-not-require-virtual-devices-to-be-cre.patch b/debian/patches/lp2060311/0012-wait-online-do-not-require-virtual-devices-to-be-cre.patch
1746deleted file mode 100644
1747index 7a34949..0000000
1748--- a/debian/patches/lp2060311/0012-wait-online-do-not-require-virtual-devices-to-be-cre.patch
1749+++ /dev/null
1750@@ -1,94 +0,0 @@
1751-From: =?utf-8?q?Lukas_M=C3=A4rdian?= <slyon@ubuntu.com>
1752-Date: Wed, 17 Apr 2024 18:01:50 +0200
1753-Subject: wait-online: do not require virtual devices to be created already
1754-
1755-Origin: https://github.com/canonical/netplan/pull/456/commits/d04f4f9a9d740cdab286ef4865b68153001b6601
1756-Bug-Ubuntu: https://bugs.launchpad.net/netplan/+bug/2060311
1757-Forwarded: https://github.com/canonical/netplan/pull/456
1758----
1759- src/networkd.c | 8 ++++++--
1760- tests/generator/test_args.py | 10 ++++++++--
1761- tests/integration/ethernets.py | 8 ++++++--
1762- 3 files changed, 20 insertions(+), 6 deletions(-)
1763-
1764-diff --git a/src/networkd.c b/src/networkd.c
1765-index f4c081c..f552bfd 100644
1766---- a/src/networkd.c
1767-+++ b/src/networkd.c
1768-@@ -1501,8 +1501,12 @@ _netplan_networkd_write_wait_online(const NetplanState* np_state, const char* ro
1769- || netplan_netdef_get_link_local_ipv6(def);
1770- _netplan_address_iter_free(addr_iter);
1771-
1772-- // no matching => single interface, ignoring non-existing interfaces
1773-- if (!netplan_netdef_has_match(def) && g_hash_table_contains(system_interfaces, def->id)) {
1774-+ // no matching => single physical interface, ignoring non-existing interfaces
1775-+ // OR: virtual interfaces, those will be created later on and cannot have a matching condition
1776-+ gboolean physical_no_match_or_virtual = FALSE
1777-+ || (!netplan_netdef_has_match(def) && g_hash_table_contains(system_interfaces, def->id))
1778-+ || (netplan_netdef_get_type(def) >= NETPLAN_DEF_TYPE_VIRTUAL);
1779-+ if (physical_no_match_or_virtual) {
1780- g_hash_table_replace(non_optional_interfaces, g_strdup(def->id), any_ips ? g_strdup("degraded") : g_strdup("carrier"));
1781- } else if (def->set_name) { // matching on a single interface, to be renamed
1782- _netplan_enumerate_interfaces(def, system_interfaces, non_optional_interfaces, any_ips ? "degraded" : "carrier", def->set_name, rootdir);
1783-diff --git a/tests/generator/test_args.py b/tests/generator/test_args.py
1784-index de5aaa8..b48291a 100644
1785---- a/tests/generator/test_args.py
1786-+++ b/tests/generator/test_args.py
1787-@@ -133,7 +133,10 @@ class TestConfigArgs(TestBase):
1788- dhcp4: true
1789- optional: true
1790- lo:
1791-- addresses: ["127.0.0.1/8", "::1/128"]''')
1792-+ addresses: ["127.0.0.1/8", "::1/128"]
1793-+ bridges:
1794-+ br0:
1795-+ dhcp4: true''')
1796- outdir = os.path.join(self.workdir.name, 'out')
1797- os.mkdir(outdir)
1798-
1799-@@ -151,6 +154,9 @@ class TestConfigArgs(TestBase):
1800- n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-lo.network')
1801- self.assertTrue(os.path.exists(n))
1802- os.unlink(n)
1803-+ n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-br0.network')
1804-+ self.assertTrue(os.path.exists(n))
1805-+ os.unlink(n)
1806-
1807- # should auto-enable networkd and -wait-online
1808- service_dir = os.path.join(self.workdir.name, 'run', 'systemd', 'system')
1809-@@ -166,7 +172,7 @@ ConditionPathIsSymbolicLink=/run/systemd/generator/network-online.target.wants/s
1810-
1811- [Service]
1812- ExecStart=
1813--ExecStart=/lib/systemd/systemd-networkd-wait-online -i lo:carrier\n''') # eth99 does not exist on the system
1814-+ExecStart=/lib/systemd/systemd-networkd-wait-online -i br0:degraded -i lo:carrier\n''') # eth99 does not exist on the system
1815-
1816- # should be a no-op the second time while the stamp exists
1817- out = subprocess.check_output([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir],
1818-diff --git a/tests/integration/ethernets.py b/tests/integration/ethernets.py
1819-index 7028a49..1e57816 100644
1820---- a/tests/integration/ethernets.py
1821-+++ b/tests/integration/ethernets.py
1822-@@ -409,8 +409,12 @@ class TestNetworkd(IntegrationTestsBase, _CommonTests):
1823- set-name: "findme"
1824- %(e2c)s:
1825- addresses: ["10.0.0.1/24"]
1826-+ bridges:
1827-+ br0:
1828-+ addresses: ["10.0.0.2/24"]
1829-+ interfaces: [%(e2c)s]
1830- ''' % {'r': self.backend, 'ec_mac': self.dev_e_client_mac, 'e2c': self.dev_e2_client})
1831-- self.generate_and_settle([self.dev_e2_client])
1832-+ self.generate_and_settle([self.dev_e2_client, 'br0'])
1833- override = os.path.join('/run', 'systemd', 'system', 'systemd-networkd-wait-online.service.d', '10-netplan.conf')
1834- self.assertTrue(os.path.isfile(override))
1835-
1836-@@ -424,7 +428,7 @@ ConditionPathIsSymbolicLink=/run/systemd/generator/network-online.target.wants/s
1837-
1838- [Service]
1839- ExecStart=
1840--ExecStart=/lib/systemd/systemd-networkd-wait-online -i %s:degraded -i findme:carrier
1841-+ExecStart=/lib/systemd/systemd-networkd-wait-online -i %s:degraded -i br0:degraded -i findme:carrier
1842- ''' % self.dev_e2_client)
1843-
1844-
1845diff --git a/debian/patches/lp2060311/0013-wait-online-recognize-that-bridge-bond-members-will-.patch b/debian/patches/lp2060311/0013-wait-online-recognize-that-bridge-bond-members-will-.patch
1846deleted file mode 100644
1847index 5f99a69..0000000
1848--- a/debian/patches/lp2060311/0013-wait-online-recognize-that-bridge-bond-members-will-.patch
1849+++ /dev/null
1850@@ -1,79 +0,0 @@
1851-From: =?utf-8?q?Lukas_M=C3=A4rdian?= <slyon@ubuntu.com>
1852-Date: Thu, 18 Apr 2024 12:16:56 +0200
1853-Subject: wait-online: recognize that bridge/bond members will never gain
1854- link-local addresses
1855-
1856-Origin: https://github.com/canonical/netplan/pull/456/commits/347d3fde2cc1c8b14c98d13d789f6ceff43b8710
1857-Bug-Ubuntu: https://bugs.launchpad.net/netplan/+bug/2060311
1858-Forwarded: https://github.com/canonical/netplan/pull/456
1859----
1860- src/networkd.c | 12 ++++++++----
1861- tests/generator/test_args.py | 16 ++++++++++++++--
1862- 2 files changed, 22 insertions(+), 6 deletions(-)
1863-
1864-diff --git a/src/networkd.c b/src/networkd.c
1865-index f552bfd..523f717 100644
1866---- a/src/networkd.c
1867-+++ b/src/networkd.c
1868-@@ -1493,12 +1493,16 @@ _netplan_networkd_write_wait_online(const NetplanState* np_state, const char* ro
1869- */
1870- if (!(def->optional || def->activation_mode)) {
1871- // Check if we have any IP configuration
1872-+ // bond and bridge members will never ask for link-local addresses (see above)
1873- struct address_iter* addr_iter = _netplan_netdef_new_address_iter(def);
1874-- gboolean any_ips = _netplan_address_iter_next(addr_iter) != NULL
1875-+ gboolean routable = _netplan_address_iter_next(addr_iter) != NULL
1876- || netplan_netdef_get_dhcp4(def)
1877-- || netplan_netdef_get_dhcp6(def)
1878-- || netplan_netdef_get_link_local_ipv4(def)
1879-- || netplan_netdef_get_link_local_ipv6(def);
1880-+ || netplan_netdef_get_dhcp6(def);
1881-+ gboolean degraded = ( netplan_netdef_get_link_local_ipv4(def)
1882-+ && !(netplan_netdef_get_bond_link(def) || netplan_netdef_get_bridge_link(def)))
1883-+ || ( netplan_netdef_get_link_local_ipv6(def)
1884-+ && !(netplan_netdef_get_bond_link(def) || netplan_netdef_get_bridge_link(def)));
1885-+ gboolean any_ips = routable || degraded;
1886- _netplan_address_iter_free(addr_iter);
1887-
1888- // no matching => single physical interface, ignoring non-existing interfaces
1889-diff --git a/tests/generator/test_args.py b/tests/generator/test_args.py
1890-index b48291a..291d164 100644
1891---- a/tests/generator/test_args.py
1892-+++ b/tests/generator/test_args.py
1893-@@ -134,9 +134,20 @@ class TestConfigArgs(TestBase):
1894- optional: true
1895- lo:
1896- addresses: ["127.0.0.1/8", "::1/128"]
1897-+ vlans:
1898-+ eth99.42:
1899-+ link: eth99
1900-+ id: 42
1901-+ link-local: [ipv4, ipv6] # this is ignored for bridge-members
1902-+ eth99.43:
1903-+ link: eth99
1904-+ id: 43
1905-+ link-local: []
1906-+ addresses: [10.0.0.2/24]
1907- bridges:
1908- br0:
1909-- dhcp4: true''')
1910-+ dhcp4: true
1911-+ interfaces: [eth99.42, eth99.43]''')
1912- outdir = os.path.join(self.workdir.name, 'out')
1913- os.mkdir(outdir)
1914-
1915-@@ -167,12 +178,13 @@ class TestConfigArgs(TestBase):
1916- override = os.path.join(service_dir, 'systemd-networkd-wait-online.service.d', '10-netplan.conf')
1917- self.assertTrue(os.path.isfile(override))
1918- with open(override, 'r') as f:
1919-+ # eth99 does not exist on the system, so will not be listed
1920- self.assertEqual(f.read(), '''[Unit]
1921- ConditionPathIsSymbolicLink=/run/systemd/generator/network-online.target.wants/systemd-networkd-wait-online.service
1922-
1923- [Service]
1924- ExecStart=
1925--ExecStart=/lib/systemd/systemd-networkd-wait-online -i br0:degraded -i lo:carrier\n''') # eth99 does not exist on the system
1926-+ExecStart=/lib/systemd/systemd-networkd-wait-online -i eth99.43:degraded -i br0:degraded -i lo:carrier -i eth99.42:carrier\n''')
1927-
1928- # should be a no-op the second time while the stamp exists
1929- out = subprocess.check_output([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir],
1930diff --git a/debian/patches/lp2065738/0014-libnetplan-use-more-restrictive-file-permissions.patch b/debian/patches/lp2065738/0014-libnetplan-use-more-restrictive-file-permissions.patch
1931deleted file mode 100644
1932index da5047c..0000000
1933--- a/debian/patches/lp2065738/0014-libnetplan-use-more-restrictive-file-permissions.patch
1934+++ /dev/null
1935@@ -1,462 +0,0 @@
1936-From: Danilo Egea Gondolfo <danilogondolfo@gmail.com>
1937-Date: Wed, 22 May 2024 15:44:16 +0100
1938-Subject: libnetplan: use more restrictive file permissions
1939-
1940-A new util.c:_netplan_g_string_free_to_file_with_permissions() was added
1941-and accepts the owner, group and file mode as arguments. When these
1942-properties can't be set, when the generator is called by a non-root user
1943-for example, it will not hard-fail. This function is called by unit
1944-tests where we can't set the owner to a privileged account for example.
1945-
1946-When generating backend files, use more restrictive permissions:
1947-
1948-networkd related files will be owned by root:systemd-network and have
1949-mode 0640.
1950-
1951-service unit files will be owned by root:root and have mode 0640.
1952-udevd files will be owned by root:root with mode 0640.
1953-
1954-wpa_supplicant and Network Manager files will continue with the existing
1955-permissions.
1956-
1957-Autopkgtests will check if the permissions are set as expected when
1958-calling the generator.
1959-
1960-This fix addresses CVE-2022-4968
1961----
1962- src/networkd.c | 40 +++++---------------
1963- src/networkd.h | 2 +
1964- src/nm.c | 4 +-
1965- src/openvswitch.c | 2 +-
1966- src/sriov.c | 4 +-
1967- src/util-internal.h | 3 ++
1968- src/util.c | 46 +++++++++++++++++++++++
1969- tests/generator/test_auth.py | 2 +-
1970- tests/generator/test_wifis.py | 2 +-
1971- tests/integration/base.py | 85 +++++++++++++++++++++++++++++++++++++++++++
1972- 10 files changed, 152 insertions(+), 38 deletions(-)
1973-
1974-diff --git a/src/networkd.c b/src/networkd.c
1975-index 2379837..cb9547c 100644
1976---- a/src/networkd.c
1977-+++ b/src/networkd.c
1978-@@ -300,7 +300,6 @@ STATIC void
1979- write_link_file(const NetplanNetDefinition* def, const char* rootdir, const char* path)
1980- {
1981- GString* s = NULL;
1982-- mode_t orig_umask;
1983-
1984- /* Don't write .link files for virtual devices; they use .netdev instead.
1985- * Don't write .link files for MODEM devices, as they aren't supported by networkd.
1986-@@ -372,9 +371,7 @@ write_link_file(const NetplanNetDefinition* def, const char* rootdir, const char
1987- g_string_append_printf(s, "LargeReceiveOffload=%s\n",
1988- (def->large_receive_offload ? "true" : "false"));
1989-
1990-- orig_umask = umask(022);
1991-- _netplan_g_string_free_to_file(s, rootdir, path, ".link");
1992-- umask(orig_umask);
1993-+ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, ".link", "root", "root", 0640);
1994- }
1995-
1996- STATIC gboolean
1997-@@ -392,7 +389,7 @@ write_regdom(const NetplanNetDefinition* def, const char* rootdir, GError** erro
1998- g_string_append(s, "\n[Service]\nType=oneshot\n");
1999- g_string_append_printf(s, "ExecStart="SBINDIR"/iw reg set %s\n", def->regulatory_domain);
2000-
2001-- _netplan_g_string_free_to_file(s, rootdir, path, NULL);
2002-+ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
2003- _netplan_safe_mkdir_p_dir(link);
2004- if (symlink(path, link) < 0 && errno != EEXIST) {
2005- // LCOV_EXCL_START
2006-@@ -572,7 +569,6 @@ STATIC void
2007- write_netdev_file(const NetplanNetDefinition* def, const char* rootdir, const char* path)
2008- {
2009- GString* s = NULL;
2010-- mode_t orig_umask;
2011-
2012- g_assert(def->type >= NETPLAN_DEF_TYPE_VIRTUAL);
2013-
2014-@@ -668,11 +664,7 @@ write_netdev_file(const NetplanNetDefinition* def, const char* rootdir, const ch
2015- default: g_assert_not_reached(); // LCOV_EXCL_LINE
2016- }
2017-
2018-- /* these do not contain secrets and need to be readable by
2019-- * systemd-networkd - LP: #1736965 */
2020-- orig_umask = umask(022);
2021-- _netplan_g_string_free_to_file(s, rootdir, path, ".netdev");
2022-- umask(orig_umask);
2023-+ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, ".netdev", "root", NETWORKD_GROUP, 0640);
2024- }
2025-
2026- STATIC void
2027-@@ -816,7 +808,6 @@ _netplan_netdef_write_network_file(
2028- g_autoptr(GString) network = NULL;
2029- g_autoptr(GString) link = NULL;
2030- GString* s = NULL;
2031-- mode_t orig_umask;
2032-
2033- SET_OPT_OUT_PTR(has_been_written, FALSE);
2034-
2035-@@ -1063,11 +1054,7 @@ _netplan_netdef_write_network_file(
2036- if (network->len > 0)
2037- g_string_append_printf(s, "\n[Network]\n%s", network->str);
2038-
2039-- /* these do not contain secrets and need to be readable by
2040-- * systemd-networkd - LP: #1736965 */
2041-- orig_umask = umask(022);
2042-- _netplan_g_string_free_to_file(s, rootdir, path, ".network");
2043-- umask(orig_umask);
2044-+ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, ".network", "root", NETWORKD_GROUP, 0640);
2045- }
2046-
2047- SET_OPT_OUT_PTR(has_been_written, TRUE);
2048-@@ -1079,7 +1066,6 @@ write_rules_file(const NetplanNetDefinition* def, const char* rootdir)
2049- {
2050- GString* s = NULL;
2051- g_autofree char* path = g_strjoin(NULL, "run/udev/rules.d/99-netplan-", def->id, ".rules", NULL);
2052-- mode_t orig_umask;
2053-
2054- /* do we need to write a .rules file?
2055- * It's only required for reliably setting the name of a physical device
2056-@@ -1113,9 +1099,7 @@ write_rules_file(const NetplanNetDefinition* def, const char* rootdir)
2057-
2058- g_string_append_printf(s, "NAME=\"%s\"\n", def->set_name);
2059-
2060-- orig_umask = umask(022);
2061-- _netplan_g_string_free_to_file(s, rootdir, path, NULL);
2062-- umask(orig_umask);
2063-+ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
2064- }
2065-
2066- STATIC gboolean
2067-@@ -1264,7 +1248,6 @@ STATIC void
2068- write_wpa_unit(const NetplanNetDefinition* def, const char* rootdir)
2069- {
2070- g_autofree gchar *stdouth = NULL;
2071-- mode_t orig_umask;
2072-
2073- stdouth = systemd_escape(def->id);
2074-
2075-@@ -1283,9 +1266,7 @@ write_wpa_unit(const NetplanNetDefinition* def, const char* rootdir)
2076- } else {
2077- g_string_append(s, " -Dnl80211,wext\n");
2078- }
2079-- orig_umask = umask(022);
2080-- _netplan_g_string_free_to_file(s, rootdir, path, NULL);
2081-- umask(orig_umask);
2082-+ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
2083- }
2084-
2085- STATIC gboolean
2086-@@ -1294,7 +1275,6 @@ write_wpa_conf(const NetplanNetDefinition* def, const char* rootdir, GError** er
2087- GHashTableIter iter;
2088- GString* s = g_string_new("ctrl_interface=/run/wpa_supplicant\n\n");
2089- g_autofree char* path = g_strjoin(NULL, "run/netplan/wpa-", def->id, ".conf", NULL);
2090-- mode_t orig_umask;
2091-
2092- g_debug("%s: Creating wpa_supplicant configuration file %s", def->id, path);
2093- if (def->type == NETPLAN_DEF_TYPE_WIFI) {
2094-@@ -1383,9 +1363,7 @@ write_wpa_conf(const NetplanNetDefinition* def, const char* rootdir, GError** er
2095- }
2096-
2097- /* use tight permissions as this contains secrets */
2098-- orig_umask = umask(077);
2099-- _netplan_g_string_free_to_file(s, rootdir, path, NULL);
2100-- umask(orig_umask);
2101-+ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0600);
2102- return TRUE;
2103- }
2104-
2105-@@ -1529,7 +1507,7 @@ _netplan_networkd_write_wait_online(const NetplanState* np_state, const char* ro
2106- GString* content = g_string_new("[Unit]\n"
2107- "ConditionPathIsSymbolicLink=/run/systemd/generator/network-online.target.wants/systemd-networkd-wait-online.service\n");
2108- if (g_hash_table_size(non_optional_interfaces) == 0) {
2109-- _netplan_g_string_free_to_file(content, rootdir, override, NULL);
2110-+ _netplan_g_string_free_to_file_with_permissions(content, rootdir, override, NULL, "root", "root", 0640);
2111- g_hash_table_destroy(non_optional_interfaces);
2112- return FALSE;
2113- }
2114-@@ -1553,7 +1531,7 @@ _netplan_networkd_write_wait_online(const NetplanState* np_state, const char* ro
2115- }
2116- g_string_append(content, "\n");
2117-
2118-- _netplan_g_string_free_to_file(content, rootdir, override, NULL);
2119-+ _netplan_g_string_free_to_file_with_permissions(content, rootdir, override, NULL, "root", "root", 0640);
2120- g_hash_table_destroy(non_optional_interfaces);
2121- return TRUE;
2122- }
2123-diff --git a/src/networkd.h b/src/networkd.h
2124-index 8332b7b..9f80585 100644
2125---- a/src/networkd.h
2126-+++ b/src/networkd.h
2127-@@ -20,6 +20,8 @@
2128- #include "netplan.h"
2129- #include <glib.h>
2130-
2131-+#define NETWORKD_GROUP "systemd-network"
2132-+
2133- NETPLAN_INTERNAL gboolean
2134- _netplan_netdef_write_networkd(
2135- const NetplanState* np_state,
2136-diff --git a/src/nm.c b/src/nm.c
2137-index 2b850af..8f1bf05 100644
2138---- a/src/nm.c
2139-+++ b/src/nm.c
2140-@@ -1150,13 +1150,13 @@ netplan_state_finish_nm_write(
2141-
2142- /* write generated NetworkManager drop-in config */
2143- if (nm_conf->len > 0)
2144-- _netplan_g_string_free_to_file(nm_conf, rootdir, "run/NetworkManager/conf.d/netplan.conf", NULL);
2145-+ _netplan_g_string_free_to_file_with_permissions(nm_conf, rootdir, "run/NetworkManager/conf.d/netplan.conf", NULL, "root", "root", 0640);
2146- else
2147- g_string_free(nm_conf, TRUE);
2148-
2149- /* write generated udev rules */
2150- if (udev_rules->len > 0)
2151-- _netplan_g_string_free_to_file(udev_rules, rootdir, "run/udev/rules.d/90-netplan.rules", NULL);
2152-+ _netplan_g_string_free_to_file_with_permissions(udev_rules, rootdir, "run/udev/rules.d/90-netplan.rules", NULL, "root", "root", 0640);
2153- else
2154- g_string_free(udev_rules, TRUE);
2155-
2156-diff --git a/src/openvswitch.c b/src/openvswitch.c
2157-index 6eb0688..2ab77e7 100644
2158---- a/src/openvswitch.c
2159-+++ b/src/openvswitch.c
2160-@@ -66,7 +66,7 @@ write_ovs_systemd_unit(const char* id, const GString* cmds, const char* rootdir,
2161- g_string_append(s, "StartLimitBurst=0\n");
2162- g_string_append(s, cmds->str);
2163-
2164-- _netplan_g_string_free_to_file(s, rootdir, path, NULL);
2165-+ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
2166-
2167- _netplan_safe_mkdir_p_dir(link);
2168- if (symlink(path, link) < 0 && errno != EEXIST) {
2169-diff --git a/src/sriov.c b/src/sriov.c
2170-index 1534c94..213f124 100644
2171---- a/src/sriov.c
2172-+++ b/src/sriov.c
2173-@@ -54,7 +54,7 @@ write_sriov_rebind_systemd_unit(GHashTable* pfs, const char* rootdir, GError** e
2174- g_string_truncate(interfaces, interfaces->len-1); /* cut trailing whitespace */
2175- g_string_append_printf(s, "ExecStart=" SBINDIR "/netplan rebind --debug %s\n", interfaces->str);
2176-
2177-- _netplan_g_string_free_to_file(s, rootdir, path, NULL);
2178-+ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
2179- g_string_free(interfaces, TRUE);
2180-
2181- _netplan_safe_mkdir_p_dir(link);
2182-@@ -90,7 +90,7 @@ write_sriov_apply_systemd_unit(GHashTable* pfs, const char* rootdir, GError** er
2183- g_string_append(s, "\n[Service]\nType=oneshot\n");
2184- g_string_append_printf(s, "ExecStart=" SBINDIR "/netplan apply --sriov-only\n");
2185-
2186-- _netplan_g_string_free_to_file(s, rootdir, path, NULL);
2187-+ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
2188-
2189- _netplan_safe_mkdir_p_dir(link);
2190- if (symlink(path, link) < 0 && errno != EEXIST) {
2191-diff --git a/src/util-internal.h b/src/util-internal.h
2192-index 86bd1b7..7454e77 100644
2193---- a/src/util-internal.h
2194-+++ b/src/util-internal.h
2195-@@ -40,6 +40,9 @@ _netplan_safe_mkdir_p_dir(const char* file_path);
2196- NETPLAN_INTERNAL void
2197- _netplan_g_string_free_to_file(GString* s, const char* rootdir, const char* path, const char* suffix);
2198-
2199-+void
2200-+_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);
2201-+
2202- NETPLAN_INTERNAL void
2203- _netplan_unlink_glob(const char* rootdir, const char* _glob);
2204-
2205-diff --git a/src/util.c b/src/util.c
2206-index 3e6c80d..46aba6e 100644
2207---- a/src/util.c
2208-+++ b/src/util.c
2209-@@ -23,6 +23,9 @@
2210- #include <regex.h>
2211- #include <string.h>
2212- #include <sys/mman.h>
2213-+#include <sys/types.h>
2214-+#include <pwd.h>
2215-+#include <grp.h>
2216-
2217- #include <glib.h>
2218- #include <glib/gprintf.h>
2219-@@ -87,6 +90,49 @@ void _netplan_g_string_free_to_file(GString* s, const char* rootdir, const char*
2220- }
2221- }
2222-
2223-+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)
2224-+{
2225-+ g_autofree char* full_path = NULL;
2226-+ g_autofree char* path_suffix = NULL;
2227-+ g_autofree char* contents = g_string_free(s, FALSE);
2228-+ GError* error = NULL;
2229-+ struct passwd* pw = NULL;
2230-+ struct group* gr = NULL;
2231-+ int ret = 0;
2232-+
2233-+ path_suffix = g_strjoin(NULL, path, suffix, NULL);
2234-+ full_path = g_build_path(G_DIR_SEPARATOR_S, rootdir ?: G_DIR_SEPARATOR_S, path_suffix, NULL);
2235-+ _netplan_safe_mkdir_p_dir(full_path);
2236-+ if (!g_file_set_contents_full(full_path, contents, -1, G_FILE_SET_CONTENTS_CONSISTENT | G_FILE_SET_CONTENTS_ONLY_EXISTING, mode, &error)) {
2237-+ /* the mkdir() just succeeded, there is no sensible
2238-+ * method to test this without root privileges, bind mounts, and
2239-+ * simulating ENOSPC */
2240-+ // LCOV_EXCL_START
2241-+ g_fprintf(stderr, "ERROR: cannot create file %s: %s\n", path, error->message);
2242-+ exit(1);
2243-+ // LCOV_EXCL_STOP
2244-+ }
2245-+
2246-+ /* Here we take the owner and group names and look up for their IDs in the passwd and group files.
2247-+ * It's OK to fail to set the owners and mode as this code will be called from unit tests.
2248-+ * The autopkgtests will check if the owner/group and mode are correctly set.
2249-+ */
2250-+ pw = getpwnam(owner);
2251-+ if (!pw) {
2252-+ g_debug("Failed to determine the UID of user %s: %s", owner, strerror(errno)); // LCOV_EXCL_LINE
2253-+ }
2254-+ gr = getgrnam(group);
2255-+ if (!gr) {
2256-+ g_debug("Failed to determine the GID of group %s: %s", group, strerror(errno)); // LCOV_EXCL_LINE
2257-+ }
2258-+ if (pw && gr) {
2259-+ ret = chown(full_path, pw->pw_uid, gr->gr_gid);
2260-+ if (ret != 0) {
2261-+ g_debug("Failed to set owner and group for file %s: %s", full_path, strerror(errno));
2262-+ }
2263-+ }
2264-+}
2265-+
2266- /**
2267- * Remove all files matching given glob.
2268- */
2269-diff --git a/tests/generator/test_auth.py b/tests/generator/test_auth.py
2270-index de23adb..d3d886c 100644
2271---- a/tests/generator/test_auth.py
2272-+++ b/tests/generator/test_auth.py
2273-@@ -226,7 +226,7 @@ network={
2274-
2275- with open(os.path.join(self.workdir.name, 'run/systemd/system/netplan-wpa-eth0.service')) as f:
2276- self.assertEqual(f.read(), SD_WPA % {'iface': 'eth0', 'drivers': 'wired'})
2277-- self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o644)
2278-+ self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o640)
2279- self.assertTrue(os.path.islink(os.path.join(
2280- self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-eth0.service')))
2281-
2282-diff --git a/tests/generator/test_wifis.py b/tests/generator/test_wifis.py
2283-index b875172..610782a 100644
2284---- a/tests/generator/test_wifis.py
2285-+++ b/tests/generator/test_wifis.py
2286-@@ -140,7 +140,7 @@ network={
2287- self.workdir.name, 'run/systemd/system/netplan-wpa-wl0.service')))
2288- with open(os.path.join(self.workdir.name, 'run/systemd/system/netplan-wpa-wl0.service')) as f:
2289- self.assertEqual(f.read(), SD_WPA % {'iface': 'wl0', 'drivers': 'nl80211,wext'})
2290-- self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o644)
2291-+ self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o640)
2292- self.assertTrue(os.path.islink(os.path.join(
2293- self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-wl0.service')))
2294-
2295-diff --git a/tests/integration/base.py b/tests/integration/base.py
2296-index e9c366c..74f0682 100644
2297---- a/tests/integration/base.py
2298-+++ b/tests/integration/base.py
2299-@@ -32,6 +32,8 @@ import shutil
2300- import gi
2301- import glob
2302- import json
2303-+import pwd
2304-+import grp
2305-
2306- # make sure we point to libnetplan properly.
2307- os.environ.update({'LD_LIBRARY_PATH': '.:{}'.format(os.environ.get('LD_LIBRARY_PATH'))})
2308-@@ -367,6 +369,89 @@ class IntegrationTestsBase(unittest.TestCase):
2309- if state:
2310- self.wait_output(['ip', 'addr', 'show', iface], state, 30)
2311-
2312-+ # Assert file permissions
2313-+ self.assert_file_permissions()
2314-+
2315-+ def assert_file_permissions(self):
2316-+ """ Check if the generated files have the expected permissions """
2317-+
2318-+ nd_expected_mode = 0o100640
2319-+ nd_expected_owner = 'root'
2320-+ nd_expected_group = 'systemd-network'
2321-+
2322-+ sd_expected_mode = 0o100640
2323-+ sd_expected_owner = 'root'
2324-+ sd_expected_group = 'root'
2325-+
2326-+ udev_expected_mode = 0o100640
2327-+ udev_expected_owner = 'root'
2328-+ udev_expected_group = 'root'
2329-+
2330-+ nm_expected_mode = 0o100600
2331-+ nm_expected_owner = 'root'
2332-+ nm_expected_group = 'root'
2333-+
2334-+ wpa_expected_mode = 0o100600
2335-+ wpa_expected_owner = 'root'
2336-+ wpa_expected_group = 'root'
2337-+
2338-+ # Check systemd-networkd files
2339-+ base_path = '/run/systemd/network'
2340-+ files = glob.glob(f'{base_path}/*.network') + glob.glob(f'{base_path}/*.netdev')
2341-+ for file in files:
2342-+ res = os.stat(file)
2343-+ user = pwd.getpwuid(res.st_uid)
2344-+ group = grp.getgrgid(res.st_gid)
2345-+ self.assertEqual(res.st_mode, nd_expected_mode, f'file {file}')
2346-+ self.assertEqual(user.pw_name, nd_expected_owner, f'file {file}')
2347-+ self.assertEqual(group.gr_name, nd_expected_group, f'file {file}')
2348-+
2349-+ # Check Network Manager files
2350-+ base_path = '/run/NetworkManager/system-connections'
2351-+ files = glob.glob(f'{base_path}/*.nmconnection')
2352-+ for file in files:
2353-+ res = os.stat(file)
2354-+ user = pwd.getpwuid(res.st_uid)
2355-+ group = grp.getgrgid(res.st_gid)
2356-+ self.assertEqual(res.st_mode, nm_expected_mode, f'file {file}')
2357-+ self.assertEqual(user.pw_name, nm_expected_owner, f'file {file}')
2358-+ self.assertEqual(group.gr_name, nm_expected_group, f'file {file}')
2359-+
2360-+ # Check wpa_supplicant configuration files
2361-+ base_path = '/run/netplan'
2362-+ files = glob.glob(f'{base_path}/wpa-*.conf')
2363-+ for file in files:
2364-+ res = os.stat(file)
2365-+ user = pwd.getpwuid(res.st_uid)
2366-+ group = grp.getgrgid(res.st_gid)
2367-+ self.assertEqual(res.st_mode, wpa_expected_mode, f'file {file}')
2368-+ self.assertEqual(user.pw_name, wpa_expected_owner, f'file {file}')
2369-+ self.assertEqual(group.gr_name, wpa_expected_group, f'file {file}')
2370-+
2371-+ # Check systemd service unit files
2372-+ base_path = '/run/systemd/system/'
2373-+ files = glob.glob(f'{base_path}/netplan-*.service')
2374-+ files += glob.glob(f'{base_path}/systemd-networkd-wait-online.service.d/*.conf')
2375-+ for file in files:
2376-+ res = os.stat(file)
2377-+ user = pwd.getpwuid(res.st_uid)
2378-+ group = grp.getgrgid(res.st_gid)
2379-+ self.assertEqual(res.st_mode, sd_expected_mode, f'file {file}')
2380-+ self.assertEqual(user.pw_name, sd_expected_owner, f'file {file}')
2381-+ self.assertEqual(group.gr_name, sd_expected_group, f'file {file}')
2382-+
2383-+ # Check systemd-udevd files
2384-+ udev_path = '/run/udev/rules.d'
2385-+ link_path = '/run/systemd/network'
2386-+ files = glob.glob(f'{udev_path}/*-netplan*.rules') + glob.glob(f'{link_path}/*.link')
2387-+ for file in files:
2388-+ res = os.stat(file)
2389-+ user = pwd.getpwuid(res.st_uid)
2390-+ group = grp.getgrgid(res.st_gid)
2391-+ self.assertEqual(res.st_mode, udev_expected_mode, f'file {file}')
2392-+ self.assertEqual(user.pw_name, udev_expected_owner, f'file {file}')
2393-+ self.assertEqual(group.gr_name, udev_expected_group, f'file {file}')
2394-+
2395- def state(self, iface, state):
2396- '''Tell generate_and_settle() to wait for a specific state'''
2397- return iface + '/' + state
2398diff --git a/debian/patches/lp2066258/0015-libnetplan-escape-control-characters.patch b/debian/patches/lp2066258/0015-libnetplan-escape-control-characters.patch
2399deleted file mode 100644
2400index 349ad9e..0000000
2401--- a/debian/patches/lp2066258/0015-libnetplan-escape-control-characters.patch
2402+++ /dev/null
2403@@ -1,867 +0,0 @@
2404-From: Danilo Egea Gondolfo <danilogondolfo@gmail.com>
2405-Date: Wed, 29 May 2024 14:50:55 +0100
2406-Subject: libnetplan: escape control characters
2407-
2408-Control characters are escaped in the parser using glib's g_strescape.
2409-Quotes and backslashes were added to the list of exception.
2410-
2411-In places where double quotes are not escaped, such as netdef IDs as it
2412-is allowed as interface names, they are escaped as needed when
2413-generating back end configuration.
2414-
2415-To support escaping in wpa_supplicant configuration, the syntax for
2416-setting the SSID was changed to 'ssid=P"string here"'. With that,
2417-escaping is support in a printf-style.
2418----
2419- src/networkd.c | 32 +++++++++-----
2420- src/nm.c | 21 ++++++----
2421- src/parse.c | 94 +++++++++++++++++++++++++++---------------
2422- src/util-internal.h | 3 ++
2423- src/util.c | 11 +++++
2424- tests/generator/test_auth.py | 20 ++++-----
2425- tests/generator/test_common.py | 42 +++++++++++++++++--
2426- tests/generator/test_wifis.py | 78 +++++++++++++++++++++++++++--------
2427- 8 files changed, 217 insertions(+), 84 deletions(-)
2428-
2429-diff --git a/src/networkd.c b/src/networkd.c
2430-index cb9547c..0414c92 100644
2431---- a/src/networkd.c
2432-+++ b/src/networkd.c
2433-@@ -1089,7 +1089,8 @@ write_rules_file(const NetplanNetDefinition* def, const char* rootdir)
2434- g_string_append(s, "SUBSYSTEM==\"net\", ACTION==\"add\", ");
2435-
2436- if (def->match.driver) {
2437-- g_string_append_printf(s,"DRIVERS==\"%s\", ", def->match.driver);
2438-+ g_autofree char* driver = _netplan_scrub_string(def->match.driver);
2439-+ g_string_append_printf(s,"DRIVERS==\"%s\", ", driver);
2440- } else {
2441- g_string_append(s, "DRIVERS==\"?*\", ");
2442- }
2443-@@ -1097,7 +1098,8 @@ write_rules_file(const NetplanNetDefinition* def, const char* rootdir)
2444- if (def->match.mac)
2445- g_string_append_printf(s, "ATTR{address}==\"%s\", ", def->match.mac);
2446-
2447-- g_string_append_printf(s, "NAME=\"%s\"\n", def->set_name);
2448-+ g_autofree char* set_name = _netplan_scrub_string(def->set_name);
2449-+ g_string_append_printf(s, "NAME=\"%s\"\n", set_name);
2450-
2451- _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
2452- }
2453-@@ -1185,10 +1187,12 @@ append_wpa_auth_conf(GString* s, const NetplanAuthenticationSettings* auth, cons
2454- }
2455-
2456- if (auth->identity) {
2457-- g_string_append_printf(s, " identity=\"%s\"\n", auth->identity);
2458-+ g_autofree char* identity = _netplan_scrub_string(auth->identity);
2459-+ g_string_append_printf(s, " identity=\"%s\"\n", identity);
2460- }
2461- if (auth->anonymous_identity) {
2462-- g_string_append_printf(s, " anonymous_identity=\"%s\"\n", auth->anonymous_identity);
2463-+ g_autofree char* anonymous_identity = _netplan_scrub_string(auth->anonymous_identity);
2464-+ g_string_append_printf(s, " anonymous_identity=\"%s\"\n", anonymous_identity);
2465- }
2466-
2467- char* psk = NULL;
2468-@@ -1226,19 +1230,23 @@ append_wpa_auth_conf(GString* s, const NetplanAuthenticationSettings* auth, cons
2469- }
2470- }
2471- if (auth->ca_certificate) {
2472-- g_string_append_printf(s, " ca_cert=\"%s\"\n", auth->ca_certificate);
2473-+ g_autofree char* ca_certificate = _netplan_scrub_string(auth->ca_certificate);
2474-+ g_string_append_printf(s, " ca_cert=\"%s\"\n", ca_certificate);
2475- }
2476- if (auth->client_certificate) {
2477-- g_string_append_printf(s, " client_cert=\"%s\"\n", auth->client_certificate);
2478-+ g_autofree char* client_certificate = _netplan_scrub_string(auth->client_certificate);
2479-+ g_string_append_printf(s, " client_cert=\"%s\"\n", client_certificate);
2480- }
2481- if (auth->client_key) {
2482-- g_string_append_printf(s, " private_key=\"%s\"\n", auth->client_key);
2483-+ g_autofree char* client_key = _netplan_scrub_string(auth->client_key);
2484-+ g_string_append_printf(s, " private_key=\"%s\"\n", client_key);
2485- }
2486- if (auth->client_key_password) {
2487- g_string_append_printf(s, " private_key_passwd=\"%s\"\n", auth->client_key_password);
2488- }
2489- if (auth->phase2_auth) {
2490-- g_string_append_printf(s, " phase2=\"auth=%s\"\n", auth->phase2_auth);
2491-+ g_autofree char* phase2_auth = _netplan_scrub_string(auth->phase2_auth);
2492-+ g_string_append_printf(s, " phase2=\"auth=%s\"\n", phase2_auth);
2493- }
2494- return TRUE;
2495- }
2496-@@ -1287,14 +1295,16 @@ write_wpa_conf(const NetplanNetDefinition* def, const char* rootdir, GError** er
2497- }
2498- /* available as of wpa_supplicant version 0.6.7 */
2499- if (def->regulatory_domain) {
2500-- g_string_append_printf(s, "country=%s\n", def->regulatory_domain);
2501-+ g_autofree char* regdom = _netplan_scrub_string(def->regulatory_domain);
2502-+ g_string_append_printf(s, "country=%s\n", regdom);
2503- }
2504- NetplanWifiAccessPoint* ap;
2505- g_hash_table_iter_init(&iter, def->access_points);
2506- while (g_hash_table_iter_next(&iter, NULL, (gpointer) &ap)) {
2507- gchar* freq_config_str = ap->mode == NETPLAN_WIFI_MODE_ADHOC ? "frequency" : "freq_list";
2508-+ g_autofree char* ssid = _netplan_scrub_string(ap->ssid);
2509-
2510-- g_string_append_printf(s, "network={\n ssid=\"%s\"\n", ap->ssid);
2511-+ g_string_append_printf(s, "network={\n ssid=P\"%s\"\n", ssid);
2512- if (ap->bssid) {
2513- g_string_append_printf(s, " bssid=%s\n", ap->bssid);
2514- }
2515-@@ -1341,7 +1351,7 @@ write_wpa_conf(const NetplanNetDefinition* def, const char* rootdir, GError** er
2516-
2517- /* wifi auth trumps netdef auth */
2518- if (ap->has_auth) {
2519-- if (!append_wpa_auth_conf(s, &ap->auth, ap->ssid, error)) {
2520-+ if (!append_wpa_auth_conf(s, &ap->auth, ssid, error)) {
2521- g_string_free(s, TRUE);
2522- return FALSE;
2523- }
2524-diff --git a/src/nm.c b/src/nm.c
2525-index 8f1bf05..cb09559 100644
2526---- a/src/nm.c
2527-+++ b/src/nm.c
2528-@@ -1082,28 +1082,30 @@ netplan_state_finish_nm_write(
2529- GString *tmp = NULL;
2530- guint unmanaged = nd->backend == NETPLAN_BACKEND_NM ? 0 : 1;
2531-
2532-+ g_autofree char* netdef_id = _netplan_scrub_string(nd->id);
2533- /* Special case: manage or ignore any device of given type on empty "match: {}" stanza */
2534- if (nd->has_match && !nd->match.driver && !nd->match.mac && !nd->match.original_name) {
2535- nm_type = type_str(nd);
2536- g_assert(nm_type);
2537- g_string_append_printf(nm_conf, "[device-netplan.%s.%s]\nmatch-device=type:%s\n"
2538- "managed=%d\n\n", netplan_def_type_name(nd->type),
2539-- nd->id, nm_type, !unmanaged);
2540-+ netdef_id, nm_type, !unmanaged);
2541- }
2542- /* Normal case: manage or ignore devices by specific udev rules */
2543- else {
2544- const gchar *prefix = "SUBSYSTEM==\"net\", ACTION==\"add|change|move\",";
2545- const gchar *suffix = nd->backend == NETPLAN_BACKEND_NM ? " ENV{NM_UNMANAGED}=\"0\"\n" : " ENV{NM_UNMANAGED}=\"1\"\n";
2546- g_string_append_printf(udev_rules, "# netplan: network.%s.%s (on NetworkManager %s)\n",
2547-- netplan_def_type_name(nd->type), nd->id,
2548-+ netplan_def_type_name(nd->type), netdef_id,
2549- unmanaged ? "deny-list" : "allow-list");
2550- /* Match by explicit interface name, if possible */
2551- if (nd->set_name) {
2552- // simple case: explicit new interface name
2553-- g_string_append_printf(udev_rules, "%s ENV{ID_NET_NAME}==\"%s\",%s", prefix, nd->set_name, suffix);
2554-+ g_autofree char* set_name = _netplan_scrub_string(nd->set_name);
2555-+ g_string_append_printf(udev_rules, "%s ENV{ID_NET_NAME}==\"%s\",%s", prefix, set_name, suffix);
2556- } else if (!nd->has_match) {
2557- // simple case: explicit netplan ID is interface name
2558-- g_string_append_printf(udev_rules, "%s ENV{ID_NET_NAME}==\"%s\",%s", prefix, nd->id, suffix);
2559-+ g_string_append_printf(udev_rules, "%s ENV{ID_NET_NAME}==\"%s\",%s", prefix, netdef_id, suffix);
2560- }
2561- /* Also, match by explicit (new) MAC, if available */
2562- if (nd->set_mac && _is_valid_macaddress(nd->set_mac)) {
2563-@@ -1119,9 +1121,10 @@ netplan_state_finish_nm_write(
2564- // match on original name glob
2565- // TODO: maybe support matching on multiple name globs in the future (like drivers)
2566- g_string_append(udev_rules, prefix);
2567-- if (nd->match.original_name)
2568-- g_string_append_printf(udev_rules, " ENV{ID_NET_NAME}==\"%s\",", nd->match.original_name);
2569--
2570-+ if (nd->match.original_name) {
2571-+ g_autofree char* original_name = _netplan_scrub_string(nd->match.original_name);
2572-+ g_string_append_printf(udev_rules, " ENV{ID_NET_NAME}==\"%s\",", original_name);
2573-+ }
2574- // match on (explicit) MAC address. Yes this would be unique on its own, but we
2575- // keep it within the "full match" to make the logic more comprehensible.
2576- if (nd->match.mac) {
2577-@@ -1139,7 +1142,9 @@ netplan_state_finish_nm_write(
2578- g_strfreev(split);
2579- } else
2580- drivers = g_strdup(nd->match.driver);
2581-- g_string_append_printf(udev_rules, " ENV{ID_NET_DRIVER}==\"%s\",", drivers);
2582-+
2583-+ g_autofree char* escaped_drivers = _netplan_scrub_string(drivers);
2584-+ g_string_append_printf(udev_rules, " ENV{ID_NET_DRIVER}==\"%s\",", escaped_drivers);
2585- g_free(drivers);
2586- }
2587- g_string_append(udev_rules, suffix);
2588-diff --git a/src/parse.c b/src/parse.c
2589-index 7f7258c..55a236c 100644
2590---- a/src/parse.c
2591-+++ b/src/parse.c
2592-@@ -55,6 +55,15 @@
2593- dst = g_strdup(src); \
2594- } }
2595-
2596-+/*
2597-+ * We use g_strescape to escape control characters from the input.
2598-+ * Besides control characters, g_strescape will also escape double quotes and backslashes.
2599-+ * Quotes are escaped at configuration generation time as needed, as they might be part of passwords for example.
2600-+ * Escaping backslashes in the parser affects "netplan set" as it will always escape \'s from
2601-+ * the input and update YAMLs with all the \'s escaped again.
2602-+*/
2603-+static char* STRESCAPE_EXCEPTIONS = "\"\\";
2604-+
2605- STATIC gboolean
2606- insert_kv_into_hash(void *key, void *value, void *hash);
2607-
2608-@@ -361,7 +370,7 @@ handle_generic_str(NetplanParser* npp, yaml_node_t* node, void* entryptr, const
2609- guint offset = GPOINTER_TO_UINT(data);
2610- char** dest = (char**) ((void*) entryptr + offset);
2611- g_free(*dest);
2612-- *dest = g_strdup(scalar(node));
2613-+ *dest = g_strescape(scalar(node), STRESCAPE_EXCEPTIONS);
2614- mark_data_as_dirty(npp, dest);
2615- return TRUE;
2616- }
2617-@@ -481,22 +490,25 @@ handle_generic_map(NetplanParser *npp, yaml_node_t* node, const char* key_prefix
2618- assert_type(npp, key, YAML_SCALAR_NODE);
2619- assert_type(npp, value, YAML_SCALAR_NODE);
2620-
2621-+ g_autofree char* escaped_key = g_strescape(scalar(key), STRESCAPE_EXCEPTIONS);
2622-+ g_autofree char* escaped_value = g_strescape(scalar(value), STRESCAPE_EXCEPTIONS);
2623-+
2624- if (key_prefix && npp->null_fields) {
2625- g_autofree char* full_key = NULL;
2626-- full_key = g_strdup_printf("%s\t%s", key_prefix, key->data.scalar.value);
2627-+ full_key = g_strdup_printf("%s\t%s", key_prefix, escaped_key);
2628- if (g_hash_table_contains(npp->null_fields, full_key))
2629- continue;
2630- }
2631-
2632- char* stored_value = NULL;
2633-- if (g_hash_table_lookup_extended(*map, scalar(key), NULL, (void**)&stored_value)) {
2634-+ if (g_hash_table_lookup_extended(*map, escaped_key, NULL, (void**)&stored_value)) {
2635- /* We can safely skip this if it is the exact key/value match
2636- * (probably caused by multi-pass processing) */
2637-- if (g_strcmp0(stored_value, scalar(value)) == 0)
2638-+ if (g_strcmp0(stored_value, escaped_value) == 0)
2639- continue;
2640-- return yaml_error(npp, node, error, "duplicate map entry '%s'", scalar(key));
2641-+ return yaml_error(npp, node, error, "duplicate map entry '%s'", escaped_key);
2642- } else
2643-- g_hash_table_insert(*map, g_strdup(scalar(key)), g_strdup(scalar(value)));
2644-+ g_hash_table_insert(*map, g_strdup(escaped_key), g_strdup(escaped_value));
2645- }
2646- mark_data_as_dirty(npp, map);
2647-
2648-@@ -519,20 +531,26 @@ handle_generic_datalist(NetplanParser *npp, yaml_node_t* node, const char* key_p
2649- for (yaml_node_pair_t* entry = node->data.mapping.pairs.start; entry < node->data.mapping.pairs.top; entry++) {
2650- yaml_node_t* key, *value;
2651- g_autofree char* full_key = NULL;
2652-+ g_autofree char* escaped_key = NULL;
2653-+ g_autofree char* escaped_value = NULL;
2654-
2655- key = yaml_document_get_node(&npp->doc, entry->key);
2656- value = yaml_document_get_node(&npp->doc, entry->value);
2657-
2658- assert_type(npp, key, YAML_SCALAR_NODE);
2659- assert_type(npp, value, YAML_SCALAR_NODE);
2660-+
2661-+ escaped_key = g_strescape(scalar(key), STRESCAPE_EXCEPTIONS);
2662-+ escaped_value = g_strescape(scalar(value), STRESCAPE_EXCEPTIONS);
2663-+
2664- if (npp->null_fields && key_prefix) {
2665-- full_key = g_strdup_printf("%s\t%s", key_prefix, scalar(key));
2666-+ full_key = g_strdup_printf("%s\t%s", key_prefix, escaped_key);
2667- if (g_hash_table_contains(npp->null_fields, full_key))
2668- continue;
2669- }
2670-
2671-- g_datalist_id_set_data_full(list, g_quark_from_string(scalar(key)),
2672-- g_strdup(scalar(value)), g_free);
2673-+ g_datalist_id_set_data_full(list, g_quark_from_string(escaped_key),
2674-+ g_strdup(escaped_value), g_free);
2675- }
2676- mark_data_as_dirty(npp, list);
2677-
2678-@@ -888,13 +906,14 @@ handle_match_driver(NetplanParser* npp, yaml_node_t* node, __unused const char*
2679- for (yaml_node_item_t *iter = node->data.sequence.items.start; iter < node->data.sequence.items.top; iter++) {
2680- elem = yaml_document_get_node(&npp->doc, *iter);
2681- assert_type(npp, elem, YAML_SCALAR_NODE);
2682-- if (g_strrstr(scalar(elem), " "))
2683-+ g_autofree char* escaped_elem = g_strescape(scalar(elem), STRESCAPE_EXCEPTIONS);
2684-+ if (g_strrstr(escaped_elem, " "))
2685- return yaml_error(npp, node, error, "A 'driver' glob cannot contain whitespace");
2686-
2687- if (!sequence)
2688-- sequence = g_string_new(scalar(elem));
2689-+ sequence = g_string_new(escaped_elem);
2690- else
2691-- g_string_append_printf(sequence, "\t%s", scalar(elem)); /* tab separated */
2692-+ g_string_append_printf(sequence, "\t%s", escaped_elem); /* tab separated */
2693- }
2694-
2695- if (!sequence)
2696-@@ -926,7 +945,7 @@ handle_auth_str(NetplanParser* npp, yaml_node_t* node, const void* data, __unuse
2697- guint offset = GPOINTER_TO_UINT(data);
2698- char** dest = (char**) ((void*) npp->current.auth + offset);
2699- g_free(*dest);
2700-- *dest = g_strdup(scalar(node));
2701-+ *dest = g_strescape(scalar(node), STRESCAPE_EXCEPTIONS);
2702- mark_data_as_dirty(npp, dest);
2703- return TRUE;
2704- }
2705-@@ -1074,7 +1093,7 @@ handle_access_point_password(NetplanParser* npp, yaml_node_t* node, __unused con
2706-
2707- access_point->auth.pmf_mode = NETPLAN_AUTH_PMF_MODE_OPTIONAL;
2708- g_free(access_point->auth.psk);
2709-- access_point->auth.psk = g_strdup(scalar(node));
2710-+ access_point->auth.psk = g_strescape(scalar(node), STRESCAPE_EXCEPTIONS);
2711- return TRUE;
2712- }
2713-
2714-@@ -1458,6 +1477,7 @@ handle_wifi_access_points(NetplanParser* npp, yaml_node_t* node, const char* key
2715- for (yaml_node_pair_t* entry = node->data.mapping.pairs.start; entry < node->data.mapping.pairs.top; entry++) {
2716- NetplanWifiAccessPoint *access_point = NULL;
2717- g_autofree char* full_key = NULL;
2718-+ g_autofree char* escaped_key = NULL;
2719- yaml_node_t* key, *value;
2720- const gchar* ssid;
2721-
2722-@@ -1466,13 +1486,15 @@ handle_wifi_access_points(NetplanParser* npp, yaml_node_t* node, const char* key
2723- value = yaml_document_get_node(&npp->doc, entry->value);
2724- assert_type(npp, value, YAML_MAPPING_NODE);
2725-
2726-+ escaped_key = g_strescape(scalar(key), STRESCAPE_EXCEPTIONS);
2727-+
2728- if (key_prefix && npp->null_fields) {
2729-- full_key = g_strdup_printf("%s\t%s", key_prefix, key->data.scalar.value);
2730-+ full_key = g_strdup_printf("%s\t%s", key_prefix, escaped_key);
2731- if (g_hash_table_contains(npp->null_fields, full_key))
2732- continue;
2733- }
2734-
2735-- ssid = scalar(key);
2736-+ ssid = escaped_key;
2737-
2738- /*
2739- * Delete the access-point if it already exists in the netdef and let the new
2740-@@ -1630,15 +1652,16 @@ handle_nameservers_search(NetplanParser* npp, yaml_node_t* node, __unused const
2741- for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) {
2742- yaml_node_t *entry = yaml_document_get_node(&npp->doc, *i);
2743- assert_type(npp, entry, YAML_SCALAR_NODE);
2744-+ g_autofree char* escaped_entry = g_strescape(scalar(entry), STRESCAPE_EXCEPTIONS);
2745-
2746- if (!npp->current.netdef->search_domains)
2747- npp->current.netdef->search_domains = g_array_new(FALSE, FALSE, sizeof(char*));
2748-
2749-- if (!is_string_in_array(npp->current.netdef->search_domains, scalar(entry))) {
2750-- char* s = g_strdup(scalar(entry));
2751-+ if (!is_string_in_array(npp->current.netdef->search_domains, escaped_entry)) {
2752-+ char* s = g_strdup(escaped_entry);
2753- g_array_append_val(npp->current.netdef->search_domains, s);
2754- } else {
2755-- g_debug("%s: Search domain '%s' has already been added", npp->current.netdef->id, scalar(entry));
2756-+ g_debug("%s: Search domain '%s' has already been added", npp->current.netdef->id, escaped_entry);
2757- }
2758- }
2759- mark_data_as_dirty(npp, &npp->current.netdef->search_domains);
2760-@@ -2744,19 +2767,19 @@ handle_ovs_bridge_controller_addresses(NetplanParser* npp, yaml_node_t* node, __
2761-
2762- /* Format: [p]unix:file */
2763- if (is_unix && vec[1] != NULL && vec[2] == NULL) {
2764-- char* s = g_strdup(scalar(entry));
2765-+ char* s = g_strescape(scalar(entry), STRESCAPE_EXCEPTIONS);
2766- g_array_append_val(npp->current.netdef->ovs_settings.controller.addresses, s);
2767- g_strfreev(vec);
2768- continue;
2769- /* Format tcp:host[:port] or ssl:host[:port] */
2770- } else if (is_host && validate_ovs_target(TRUE, vec[1])) {
2771-- char* s = g_strdup(scalar(entry));
2772-+ char* s = g_strescape(scalar(entry), STRESCAPE_EXCEPTIONS);
2773- g_array_append_val(npp->current.netdef->ovs_settings.controller.addresses, s);
2774- g_strfreev(vec);
2775- continue;
2776- /* Format ptcp:[port][:host] or pssl:[port][:host] */
2777- } else if (is_port && validate_ovs_target(FALSE, vec[1])) {
2778-- char* s = g_strdup(scalar(entry));
2779-+ char* s = g_strescape(scalar(entry), STRESCAPE_EXCEPTIONS);
2780- g_array_append_val(npp->current.netdef->ovs_settings.controller.addresses, s);
2781- g_strfreev(vec);
2782- continue;
2783-@@ -3070,6 +3093,8 @@ handle_network_ovs_settings_global_ports(NetplanParser* npp, yaml_node_t* node,
2784- NetplanNetDefinition *component2 = NULL;
2785-
2786- for (yaml_node_item_t *iter = node->data.sequence.items.start; iter < node->data.sequence.items.top; iter++) {
2787-+ g_autofree char* escaped_port = NULL;
2788-+ g_autofree char* escaped_peer = NULL;
2789- pair = yaml_document_get_node(&npp->doc, *iter);
2790- assert_type(npp, pair, YAML_SEQUENCE_NODE);
2791-
2792-@@ -3084,14 +3109,17 @@ handle_network_ovs_settings_global_ports(NetplanParser* npp, yaml_node_t* node,
2793- peer = yaml_document_get_node(&npp->doc, *(item+1));
2794- assert_type(npp, peer, YAML_SCALAR_NODE);
2795-
2796-- if (!g_strcmp0(scalar(port), scalar(peer)))
2797-+ escaped_port = g_strescape(scalar(port), STRESCAPE_EXCEPTIONS);
2798-+ escaped_peer = g_strescape(scalar(peer), STRESCAPE_EXCEPTIONS);
2799-+
2800-+ if (!g_strcmp0(escaped_port, escaped_peer))
2801- return yaml_error(npp, peer, error, "Open vSwitch patch ports must be of different name");
2802-
2803- /* Create port 1 netdef */
2804-- component1 = npp->parsed_defs ? g_hash_table_lookup(npp->parsed_defs, scalar(port)) : NULL;
2805-+ component1 = npp->parsed_defs ? g_hash_table_lookup(npp->parsed_defs, escaped_port) : NULL;
2806- if (!component1) {
2807-- component1 = netplan_netdef_new(npp, scalar(port), NETPLAN_DEF_TYPE_PORT, NETPLAN_BACKEND_OVS);
2808-- if (g_hash_table_remove(npp->missing_id, scalar(port)))
2809-+ component1 = netplan_netdef_new(npp, escaped_port, NETPLAN_DEF_TYPE_PORT, NETPLAN_BACKEND_OVS);
2810-+ if (g_hash_table_remove(npp->missing_id, escaped_port))
2811- npp->missing_ids_found++;
2812- }
2813-
2814-@@ -3102,15 +3130,15 @@ handle_network_ovs_settings_global_ports(NetplanParser* npp, yaml_node_t* node,
2815- component1->filepath = g_strdup(npp->current.filepath);
2816- }
2817-
2818-- if (component1->peer && g_strcmp0(component1->peer, scalar(peer)))
2819-+ if (component1->peer && g_strcmp0(component1->peer, escaped_peer))
2820- return yaml_error(npp, port, error, "Open vSwitch port '%s' is already assigned to peer '%s'",
2821- component1->id, component1->peer);
2822-
2823- /* Create port 2 (peer) netdef */
2824-- component2 = npp->parsed_defs ? g_hash_table_lookup(npp->parsed_defs, scalar(peer)) : NULL;
2825-+ component2 = npp->parsed_defs ? g_hash_table_lookup(npp->parsed_defs, escaped_peer) : NULL;
2826- if (!component2) {
2827-- component2 = netplan_netdef_new(npp, scalar(peer), NETPLAN_DEF_TYPE_PORT, NETPLAN_BACKEND_OVS);
2828-- if (g_hash_table_remove(npp->missing_id, scalar(peer)))
2829-+ component2 = netplan_netdef_new(npp, escaped_peer, NETPLAN_DEF_TYPE_PORT, NETPLAN_BACKEND_OVS);
2830-+ if (g_hash_table_remove(npp->missing_id, escaped_peer))
2831- npp->missing_ids_found++;
2832- }
2833-
2834-@@ -3121,16 +3149,16 @@ handle_network_ovs_settings_global_ports(NetplanParser* npp, yaml_node_t* node,
2835- component2->filepath = g_strdup(npp->current.filepath);
2836- }
2837-
2838-- if (component2->peer && g_strcmp0(component2->peer, scalar(port)))
2839-+ if (component2->peer && g_strcmp0(component2->peer, escaped_port))
2840- return yaml_error(npp, peer, error, "Open vSwitch port '%s' is already assigned to peer '%s'",
2841- component2->id, component2->peer);
2842-
2843- if (!component1->peer) {
2844-- component1->peer = g_strdup(scalar(peer));
2845-+ component1->peer = g_strdup(escaped_peer);
2846- component1->peer_link = component2;
2847- }
2848- if (!component2->peer) {
2849-- component2->peer = g_strdup(scalar(port));
2850-+ component2->peer = g_strdup(escaped_port);
2851- component2->peer_link = component1;
2852- }
2853- }
2854-diff --git a/src/util-internal.h b/src/util-internal.h
2855-index 7454e77..3b33c03 100644
2856---- a/src/util-internal.h
2857-+++ b/src/util-internal.h
2858-@@ -185,3 +185,6 @@ _netplan_netdef_pertype_iter_next(struct netdef_pertype_iter* it);
2859-
2860- NETPLAN_INTERNAL void
2861- _netplan_netdef_pertype_iter_free(struct netdef_pertype_iter* it);
2862-+
2863-+gchar*
2864-+_netplan_scrub_string(const char* content);
2865-diff --git a/src/util.c b/src/util.c
2866-index 46aba6e..86f8c1f 100644
2867---- a/src/util.c
2868-+++ b/src/util.c
2869-@@ -1230,3 +1230,14 @@ _is_valid_macaddress(const char* value)
2870-
2871- return regexec(&re, value, 0, NULL, 0) == 0;
2872- }
2873-+
2874-+gchar*
2875-+_netplan_scrub_string(const char* content)
2876-+{
2877-+ GString* s = g_string_new(content);
2878-+
2879-+ // Escape double quotes
2880-+ g_string_replace(s, "\"", "\\\"", 0);
2881-+
2882-+ return g_string_free(s, FALSE);
2883-+}
2884-diff --git a/tests/generator/test_auth.py b/tests/generator/test_auth.py
2885-index d3d886c..bf0108a 100644
2886---- a/tests/generator/test_auth.py
2887-+++ b/tests/generator/test_auth.py
2888-@@ -92,21 +92,21 @@ class TestNetworkd(TestBase):
2889- self.assertIn('ctrl_interface=/run/wpa_supplicant', new_config)
2890- self.assertIn('''
2891- network={
2892-- ssid="peer2peer"
2893-+ ssid=P"peer2peer"
2894- mode=1
2895- key_mgmt=NONE
2896- }
2897- ''', new_config)
2898- self.assertIn('''
2899- network={
2900-- ssid="Luke's Home"
2901-+ ssid=P"Luke's Home"
2902- key_mgmt=WPA-PSK
2903- psk="4lsos3kr1t"
2904- }
2905- ''', new_config)
2906- self.assertIn('''
2907- network={
2908-- ssid="BobsHome"
2909-+ ssid=P"BobsHome"
2910- key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
2911- ieee80211w=1
2912- psk=e03ce667c87bc81ca968d9120ca37f89eb09aec3c55b80386e5d772efd6b926e
2913-@@ -114,14 +114,14 @@ network={
2914- ''', new_config)
2915- self.assertIn('''
2916- network={
2917-- ssid="BillsHome"
2918-+ ssid=P"BillsHome"
2919- key_mgmt=WPA-PSK
2920- psk=db3b0acf5653aeaddd5fe034fb9f07175b2864f847b005aaa2f09182d9411b04
2921- }
2922- ''', new_config)
2923- self.assertIn('''
2924- network={
2925-- ssid="workplace2"
2926-+ ssid=P"workplace2"
2927- key_mgmt=WPA-EAP
2928- eap=PEAP
2929- identity="joe@internal.example.com"
2930-@@ -131,7 +131,7 @@ network={
2931- ''', new_config)
2932- self.assertIn('''
2933- network={
2934-- ssid="workplace"
2935-+ ssid=P"workplace"
2936- key_mgmt=WPA-EAP
2937- eap=TTLS
2938- identity="joe@internal.example.com"
2939-@@ -141,7 +141,7 @@ network={
2940- ''', new_config)
2941- self.assertIn('''
2942- network={
2943-- ssid="workplacehashed"
2944-+ ssid=P"workplacehashed"
2945- key_mgmt=WPA-EAP
2946- eap=TTLS
2947- identity="joe@internal.example.com"
2948-@@ -151,7 +151,7 @@ network={
2949- ''', new_config)
2950- self.assertIn('''
2951- network={
2952-- ssid="customernet"
2953-+ ssid=P"customernet"
2954- key_mgmt=WPA-EAP
2955- eap=TLS
2956- identity="cert-joe@cust.example.com"
2957-@@ -164,13 +164,13 @@ network={
2958- ''', new_config)
2959- self.assertIn('''
2960- network={
2961-- ssid="opennet"
2962-+ ssid=P"opennet"
2963- key_mgmt=NONE
2964- }
2965- ''', new_config)
2966- self.assertIn('''
2967- network={
2968-- ssid="Joe's Home"
2969-+ ssid=P"Joe's Home"
2970- key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
2971- ieee80211w=1
2972- psk="s0s3kr1t"
2973-diff --git a/tests/generator/test_common.py b/tests/generator/test_common.py
2974-index d98eb32..8dfaf7d 100644
2975---- a/tests/generator/test_common.py
2976-+++ b/tests/generator/test_common.py
2977-@@ -19,7 +19,7 @@
2978- import os
2979- import textwrap
2980-
2981--from .base import TestBase, ND_DHCP4, ND_DHCP6, ND_DHCPYES, ND_EMPTY, NM_MANAGED, NM_UNMANAGED
2982-+from .base import UDEV_NO_MAC_RULE, TestBase, ND_DHCP4, ND_DHCP6, ND_DHCPYES, ND_EMPTY, NM_MANAGED, NM_UNMANAGED
2983-
2984-
2985- class TestNetworkd(TestBase):
2986-@@ -810,6 +810,18 @@ RouteMetric=100
2987- UseMTU=true
2988- '''})
2989-
2990-+ def test_nd_udev_rules_escaped(self):
2991-+ self.generate('''network:
2992-+ version: 2
2993-+ renderer: NetworkManager
2994-+ ethernets:
2995-+ def1:
2996-+ match:
2997-+ driver: "abc\\"xyz\\n0\\n\\n1"
2998-+ set-name: "eth\\"\\n\\nxyz\\n0"''', skip_generated_yaml_validation=True)
2999-+
3000-+ self.assert_networkd_udev({'def1.rules': (UDEV_NO_MAC_RULE % ('abc\\"xyz\\n0\\n\\n1', 'eth\\"\\n\\nxyz\\n0'))})
3001-+
3002-
3003- class TestNetworkManager(TestBase):
3004-
3005-@@ -1315,6 +1327,28 @@ dns=8.8.8.8;
3006- method=ignore
3007- '''})
3008-
3009-+ def test_nm_udev_rules_escaped(self):
3010-+ self.generate('''network:
3011-+ version: 2
3012-+ renderer: networkd
3013-+ ethernets:
3014-+ eth0:
3015-+ match:
3016-+ name: "eth\\"0"
3017-+ dhcp4: true''')
3018-+ self.assert_nm_udev(NM_UNMANAGED % 'eth\\"0')
3019-+
3020-+ self.generate('''network:
3021-+ version: 2
3022-+ renderer: networkd
3023-+ ethernets:
3024-+ eth0:
3025-+ match:
3026-+ name: "eth0"
3027-+ set-name: "eth\\n0"
3028-+ dhcp4: true''', skip_generated_yaml_validation=True)
3029-+ self.assert_nm_udev(NM_UNMANAGED % 'eth\\n0' + NM_UNMANAGED % 'eth0')
3030-+
3031-
3032- class TestForwardDeclaration(TestBase):
3033-
3034-@@ -1777,13 +1811,13 @@ LinkLocalAddressing=ipv6
3035- self.assert_wpa_supplicant("wlan0", """ctrl_interface=/run/wpa_supplicant
3036-
3037- network={
3038-- ssid="mynewwifi"
3039-+ ssid=P"mynewwifi"
3040- key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
3041- ieee80211w=1
3042- psk="aaaaaaaa"
3043- }
3044- network={
3045-- ssid="mywifi"
3046-+ ssid=P"mywifi"
3047- key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
3048- ieee80211w=1
3049- psk="aaaaaaaa"
3050-@@ -1814,7 +1848,7 @@ network={
3051- self.assert_wpa_supplicant("wlan0", """ctrl_interface=/run/wpa_supplicant
3052-
3053- network={
3054-- ssid="mywifi"
3055-+ ssid=P"mywifi"
3056- key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
3057- ieee80211w=1
3058- psk="bbbbbbbb"
3059-diff --git a/tests/generator/test_wifis.py b/tests/generator/test_wifis.py
3060-index 610782a..9f7359b 100644
3061---- a/tests/generator/test_wifis.py
3062-+++ b/tests/generator/test_wifis.py
3063-@@ -65,7 +65,7 @@ class TestNetworkd(TestBase):
3064- with open(os.path.join(self.workdir.name, 'run/netplan/wpa-wl0.conf')) as f:
3065- new_config = f.read()
3066-
3067-- network = 'ssid="{}"\n freq_list='.format('band-no-channel2')
3068-+ network = 'ssid=P"{}"\n freq_list='.format('band-no-channel2')
3069- freqs_5GHz = [5610, 5310, 5620, 5320, 5630, 5640, 5340, 5035, 5040, 5045, 5055, 5060, 5660, 5680, 5670, 5080, 5690,
3070- 5700, 5710, 5720, 5825, 5745, 5755, 5805, 5765, 5160, 5775, 5170, 5480, 5180, 5795, 5190, 5500, 5200,
3071- 5510, 5210, 5520, 5220, 5530, 5230, 5540, 5240, 5550, 5250, 5560, 5260, 5570, 5270, 5580, 5280, 5590,
3072-@@ -76,7 +76,7 @@ class TestNetworkd(TestBase):
3073- for freq in freqs_5GHz:
3074- self.assertRegex(new_config, '{}[ 0-9]*{}[ 0-9]*\n'.format(network, freq))
3075-
3076-- network = 'ssid="{}"\n freq_list='.format('band-no-channel')
3077-+ network = 'ssid=P"{}"\n freq_list='.format('band-no-channel')
3078- freqs_24GHz = [2412, 2417, 2422, 2427, 2432, 2442, 2447, 2437, 2452, 2457, 2462, 2467, 2472, 2484]
3079- freqs = new_config.split(network)
3080- freqs = freqs[1].split('\n')[0]
3081-@@ -86,20 +86,20 @@ class TestNetworkd(TestBase):
3082-
3083- self.assertIn('''
3084- network={
3085-- ssid="channel-no-band"
3086-+ ssid=P"channel-no-band"
3087- key_mgmt=NONE
3088- }
3089- ''', new_config)
3090- self.assertIn('''
3091- network={
3092-- ssid="peer2peer"
3093-+ ssid=P"peer2peer"
3094- mode=1
3095- key_mgmt=NONE
3096- }
3097- ''', new_config)
3098- self.assertIn('''
3099- network={
3100-- ssid="hidden-y"
3101-+ ssid=P"hidden-y"
3102- scan_ssid=1
3103- key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
3104- ieee80211w=1
3105-@@ -108,7 +108,7 @@ network={
3106- ''', new_config)
3107- self.assertIn('''
3108- network={
3109-- ssid="hidden-n"
3110-+ ssid=P"hidden-n"
3111- key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
3112- ieee80211w=1
3113- psk="5ecur1ty"
3114-@@ -116,7 +116,7 @@ network={
3115- ''', new_config)
3116- self.assertIn('''
3117- network={
3118-- ssid="workplace"
3119-+ ssid=P"workplace"
3120- bssid=de:ad:be:ef:ca:fe
3121- freq_list=5500
3122- key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
3123-@@ -126,7 +126,7 @@ network={
3124- ''', new_config)
3125- self.assertIn('''
3126- network={
3127-- ssid="Joe's Home"
3128-+ ssid=P"Joe's Home"
3129- bssid=00:11:22:33:44:55
3130- freq_list=2462
3131- key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
3132-@@ -303,7 +303,7 @@ LinkLocalAddressing=ipv6
3133- self.assertIn('''
3134- wowlan_triggers=any disconnect magic_pkt gtk_rekey_failure eap_identity_req four_way_handshake rfkill_release
3135- network={
3136-- ssid="homenet"
3137-+ ssid=P"homenet"
3138- key_mgmt=NONE
3139- }
3140- ''', new_config)
3141-@@ -336,7 +336,7 @@ LinkLocalAddressing=ipv6
3142- new_config = f.read()
3143- self.assertIn('''
3144- network={
3145-- ssid="homenet"
3146-+ ssid=P"homenet"
3147- key_mgmt=NONE
3148- }
3149- ''', new_config)
3150-@@ -360,7 +360,7 @@ network={
3151- self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant
3152-
3153- network={
3154-- ssid="homenet"
3155-+ ssid=P"homenet"
3156- key_mgmt=SAE
3157- ieee80211w=2
3158- psk="********"
3159-@@ -387,7 +387,7 @@ network={
3160- self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant
3161-
3162- network={
3163-- ssid="homenet"
3164-+ ssid=P"homenet"
3165- key_mgmt=WPA-EAP WPA-EAP-SHA256
3166- eap=TLS
3167- ieee80211w=1
3168-@@ -420,7 +420,7 @@ network={
3169- self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant
3170-
3171- network={
3172-- ssid="homenet"
3173-+ ssid=P"homenet"
3174- key_mgmt=WPA-EAP-SUITE-B-192
3175- eap=TLS
3176- ieee80211w=2
3177-@@ -449,7 +449,7 @@ network={
3178- self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant
3179-
3180- network={
3181-- ssid="homenet"
3182-+ ssid=P"homenet"
3183- key_mgmt=IEEE8021X
3184- eap=LEAP
3185- identity="some-id"
3186-@@ -473,7 +473,7 @@ network={
3187- self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant
3188-
3189- network={
3190-- ssid="homenet"
3191-+ ssid=P"homenet"
3192- key_mgmt=IEEE8021X
3193- eap=PWD
3194- identity="some-id"
3195-@@ -498,7 +498,7 @@ network={
3196- self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant
3197-
3198- network={
3199-- ssid="homenet"
3200-+ ssid=P"homenet"
3201- key_mgmt=WPA-EAP
3202- eap=LEAP
3203- ieee80211w=1
3204-@@ -508,6 +508,48 @@ network={
3205- }
3206- """)
3207-
3208-+ def test_escaping_special_characters(self):
3209-+ self.generate('''network:
3210-+ version: 2
3211-+ wifis:
3212-+ wl0:
3213-+ regulatory-domain: "abc\\n\\n321\\n\\"123"
3214-+ access-points:
3215-+ "abc\\n\\n123\\"x\\ry\\bz":
3216-+ password: "abc\\n\\n\\n\\"123"
3217-+ auth:
3218-+ key-management: eap
3219-+ method: leap
3220-+ anonymous-identity: "abc\\n\\n321\\n\\"123"
3221-+ identity: "abc\\n\\n321\\n\\"123"
3222-+ password: "abc\\n\\n\\n\\"123"
3223-+ ca-certificate: "abc\\n\\n321\\n\\"123"
3224-+ client-certificate: "abc\\n\\n321\\n\\"123"
3225-+ client-key: "abc\\n\\n321\\n\\"123"
3226-+ client-key-password: "abc\\n\\n321\\n\\"123"
3227-+ phase2-auth: "abc\\n\\n321\\n\\"123"
3228-+ ''', skip_generated_yaml_validation=True)
3229-+
3230-+ self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant
3231-+
3232-+country=abc\\n\\n321\\n\\"123
3233-+network={
3234-+ ssid=P"abc\\n\\n123\\\"x\\ry\\bz"
3235-+ key_mgmt=WPA-EAP
3236-+ eap=LEAP
3237-+ ieee80211w=1
3238-+ identity="abc\\n\\n321\\n\\\"123"
3239-+ anonymous_identity="abc\\n\\n321\\n\\\"123"
3240-+ psk="abc\\n\\n\\n\"123"
3241-+ password="abc\\n\\n\\n\"123"
3242-+ ca_cert="abc\\n\\n321\\n\\\"123"
3243-+ client_cert="abc\\n\\n321\\n\\\"123"
3244-+ private_key="abc\\n\\n321\\n\\\"123"
3245-+ private_key_passwd="abc\\n\\n321\\n\"123"
3246-+ phase2="auth=abc\\n\\n321\\n\\\"123"
3247-+}
3248-+""")
3249-+
3250-
3251- class TestNetworkManager(TestBase):
3252-
3253-@@ -790,7 +832,7 @@ mode=adhoc
3254- self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant
3255-
3256- network={
3257-- ssid="homenet"
3258-+ ssid=P"homenet"
3259- frequency=2442
3260- mode=1
3261- key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
3262-@@ -814,7 +856,7 @@ network={
3263- self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant
3264-
3265- network={
3266-- ssid="homenet"
3267-+ ssid=P"homenet"
3268- frequency=5035
3269- mode=1
3270- key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
3271diff --git a/debian/patches/lp2066258/0016-backends-escape-file-paths.patch b/debian/patches/lp2066258/0016-backends-escape-file-paths.patch
3272deleted file mode 100644
3273index 1e42442..0000000
3274--- a/debian/patches/lp2066258/0016-backends-escape-file-paths.patch
3275+++ /dev/null
3276@@ -1,288 +0,0 @@
3277-From: Danilo Egea Gondolfo <danilogondolfo@gmail.com>
3278-Date: Thu, 23 May 2024 15:54:43 +0100
3279-Subject: backends: escape file paths
3280-
3281-Escape strings used to build paths with g_uri_escape_string().
3282-systemd_escape() could also be used but it has the downside of calling
3283-an external program and, by default, it escapes dashes (which are
3284-present in files generated from Network Manager for example).
3285----
3286- src/networkd.c | 13 +++++----
3287- src/nm.c | 5 ++--
3288- src/openvswitch.c | 19 ++++++------
3289- src/util.c | 7 +++--
3290- tests/generator/test_common.py | 66 ++++++++++++++++++++++++++++++++++++++++++
3291- tests/generator/test_ovs.py | 24 +++++++++++++++
3292- 6 files changed, 115 insertions(+), 19 deletions(-)
3293-
3294-diff --git a/src/networkd.c b/src/networkd.c
3295-index 0414c92..1da684b 100644
3296---- a/src/networkd.c
3297-+++ b/src/networkd.c
3298-@@ -1065,7 +1065,8 @@ STATIC void
3299- write_rules_file(const NetplanNetDefinition* def, const char* rootdir)
3300- {
3301- GString* s = NULL;
3302-- g_autofree char* path = g_strjoin(NULL, "run/udev/rules.d/99-netplan-", def->id, ".rules", NULL);
3303-+ g_autofree char* escaped_netdef_id = g_uri_escape_string(def->id, NULL, TRUE);
3304-+ g_autofree char* path = g_strjoin(NULL, "run/udev/rules.d/99-netplan-", escaped_netdef_id, ".rules", NULL);
3305-
3306- /* do we need to write a .rules file?
3307- * It's only required for reliably setting the name of a physical device
3308-@@ -1282,7 +1283,8 @@ write_wpa_conf(const NetplanNetDefinition* def, const char* rootdir, GError** er
3309- {
3310- GHashTableIter iter;
3311- GString* s = g_string_new("ctrl_interface=/run/wpa_supplicant\n\n");
3312-- g_autofree char* path = g_strjoin(NULL, "run/netplan/wpa-", def->id, ".conf", NULL);
3313-+ g_autofree char* escaped_netdef_id = g_uri_escape_string(def->id, NULL, TRUE);
3314-+ g_autofree char* path = g_strjoin(NULL, "run/netplan/wpa-", escaped_netdef_id, ".conf", NULL);
3315-
3316- g_debug("%s: Creating wpa_supplicant configuration file %s", def->id, path);
3317- if (def->type == NETPLAN_DEF_TYPE_WIFI) {
3318-@@ -1394,7 +1396,8 @@ _netplan_netdef_write_networkd(
3319- GError** error)
3320- {
3321- /* TODO: make use of netplan_netdef_get_output_filename() */
3322-- g_autofree char* path_base = g_strjoin(NULL, "run/systemd/network/10-netplan-", def->id, NULL);
3323-+ g_autofree char* escaped_netdef_id = g_uri_escape_string(def->id, NULL, TRUE);
3324-+ g_autofree char* path_base = g_strjoin(NULL, "run/systemd/network/10-netplan-", escaped_netdef_id, NULL);
3325- SET_OPT_OUT_PTR(has_been_written, FALSE);
3326-
3327- /* We want this for all backends when renaming, as *.link and *.rules files are
3328-@@ -1416,8 +1419,8 @@ _netplan_netdef_write_networkd(
3329- }
3330-
3331- if (def->type == NETPLAN_DEF_TYPE_WIFI || def->has_auth) {
3332-- g_autofree char* link = g_strjoin(NULL, rootdir ?: "", "/run/systemd/system/systemd-networkd.service.wants/netplan-wpa-", def->id, ".service", NULL);
3333-- g_autofree char* slink = g_strjoin(NULL, "/run/systemd/system/netplan-wpa-", def->id, ".service", NULL);
3334-+ g_autofree char* link = g_strjoin(NULL, rootdir ?: "", "/run/systemd/system/systemd-networkd.service.wants/netplan-wpa-", escaped_netdef_id, ".service", NULL);
3335-+ g_autofree char* slink = g_strjoin(NULL, "/run/systemd/system/netplan-wpa-", escaped_netdef_id, ".service", NULL);
3336- if (def->type == NETPLAN_DEF_TYPE_WIFI && def->has_match) {
3337- 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);
3338- return FALSE;
3339-diff --git a/src/nm.c b/src/nm.c
3340-index cb09559..aa22567 100644
3341---- a/src/nm.c
3342-+++ b/src/nm.c
3343-@@ -932,10 +932,11 @@ write_nm_conf_access_point(const NetplanNetDefinition* def, const char* rootdir,
3344- g_datalist_foreach((GData**)&def->backend_settings.passthrough, write_fallback_key_value, kf);
3345- }
3346-
3347-+ g_autofree char* escaped_netdef_id = g_uri_escape_string(def->id, NULL, TRUE);
3348- if (ap) {
3349- g_autofree char* escaped_ssid = g_uri_escape_string(ap->ssid, NULL, TRUE);
3350- /* TODO: make use of netplan_netdef_get_output_filename() */
3351-- conf_path = g_strjoin(NULL, "run/NetworkManager/system-connections/netplan-", def->id, "-", escaped_ssid, ".nmconnection", NULL);
3352-+ conf_path = g_strjoin(NULL, "run/NetworkManager/system-connections/netplan-", escaped_netdef_id, "-", escaped_ssid, ".nmconnection", NULL);
3353-
3354- g_key_file_set_string(kf, "wifi", "ssid", ap->ssid);
3355- if (ap->mode < NETPLAN_WIFI_MODE_OTHER)
3356-@@ -970,7 +971,7 @@ write_nm_conf_access_point(const NetplanNetDefinition* def, const char* rootdir,
3357- }
3358- } else {
3359- /* TODO: make use of netplan_netdef_get_output_filename() */
3360-- conf_path = g_strjoin(NULL, "run/NetworkManager/system-connections/netplan-", def->id, ".nmconnection", NULL);
3361-+ conf_path = g_strjoin(NULL, "run/NetworkManager/system-connections/netplan-", escaped_netdef_id, ".nmconnection", NULL);
3362- if (def->has_auth) {
3363- write_dot1x_auth_parameters(&def->auth, kf);
3364- }
3365-diff --git a/src/openvswitch.c b/src/openvswitch.c
3366-index 2ab77e7..0a81a59 100644
3367---- a/src/openvswitch.c
3368-+++ b/src/openvswitch.c
3369-@@ -31,9 +31,9 @@
3370- STATIC gboolean
3371- write_ovs_systemd_unit(const char* id, const GString* cmds, const char* rootdir, gboolean physical, gboolean cleanup, const char* dependency, GError** error)
3372- {
3373-- g_autofree gchar* id_escaped = NULL;
3374-- g_autofree char* link = g_strjoin(NULL, rootdir ?: "", "/run/systemd/system/systemd-networkd.service.wants/netplan-ovs-", id, ".service", NULL);
3375-- g_autofree char* path = g_strjoin(NULL, "/run/systemd/system/netplan-ovs-", id, ".service", NULL);
3376-+ g_autofree char* escaped_netdef_id = g_uri_escape_string(id, NULL, TRUE);
3377-+ g_autofree char* link = g_strjoin(NULL, rootdir ?: "", "/run/systemd/system/systemd-networkd.service.wants/netplan-ovs-", escaped_netdef_id, ".service", NULL);
3378-+ g_autofree char* path = g_strjoin(NULL, "/run/systemd/system/netplan-ovs-", escaped_netdef_id, ".service", NULL);
3379-
3380- GString* s = g_string_new("[Unit]\n");
3381- g_string_append_printf(s, "Description=OpenVSwitch configuration for %s\n", id);
3382-@@ -42,9 +42,8 @@ write_ovs_systemd_unit(const char* id, const GString* cmds, const char* rootdir,
3383- g_string_append_printf(s, "Wants=ovsdb-server.service\n");
3384- g_string_append_printf(s, "After=ovsdb-server.service\n");
3385- if (physical) {
3386-- id_escaped = systemd_escape((char*) id);
3387-- g_string_append_printf(s, "Requires=sys-subsystem-net-devices-%s.device\n", id_escaped);
3388-- g_string_append_printf(s, "After=sys-subsystem-net-devices-%s.device\n", id_escaped);
3389-+ g_string_append_printf(s, "Requires=sys-subsystem-net-devices-%s.device\n", escaped_netdef_id);
3390-+ g_string_append_printf(s, "After=sys-subsystem-net-devices-%s.device\n", escaped_netdef_id);
3391- }
3392- if (!cleanup) {
3393- g_string_append_printf(s, "After=netplan-ovs-cleanup.service\n");
3394-@@ -54,8 +53,9 @@ write_ovs_systemd_unit(const char* id, const GString* cmds, const char* rootdir,
3395- }
3396- g_string_append(s, "Before=network.target\nWants=network.target\n");
3397- if (dependency) {
3398-- g_string_append_printf(s, "Requires=netplan-ovs-%s.service\n", dependency);
3399-- g_string_append_printf(s, "After=netplan-ovs-%s.service\n", dependency);
3400-+ g_autofree char* escaped_dependency = g_uri_escape_string(dependency, NULL, TRUE);
3401-+ g_string_append_printf(s, "Requires=netplan-ovs-%s.service\n", escaped_dependency);
3402-+ g_string_append_printf(s, "After=netplan-ovs-%s.service\n", escaped_dependency);
3403- }
3404-
3405- g_string_append(s, "\n[Service]\nType=oneshot\nTimeoutStartSec=10s\n");
3406-@@ -325,6 +325,7 @@ _netplan_netdef_write_ovs(const NetplanState* np_state, const NetplanNetDefiniti
3407- gchar* dependency = NULL;
3408- const char* type = netplan_type_to_table_name(def->type);
3409- g_autofree char* base_config_path = NULL;
3410-+ g_autofree char* escaped_netdef_id = g_uri_escape_string(def->id, NULL, TRUE);
3411- char* value = NULL;
3412- const NetplanOVSSettings* settings = &np_state->ovs_settings;
3413-
3414-@@ -415,7 +416,7 @@ _netplan_netdef_write_ovs(const NetplanState* np_state, const NetplanNetDefiniti
3415-
3416- /* Try writing out a base config */
3417- /* TODO: make use of netplan_netdef_get_output_filename() */
3418-- base_config_path = g_strjoin(NULL, "run/systemd/network/10-netplan-", def->id, NULL);
3419-+ base_config_path = g_strjoin(NULL, "run/systemd/network/10-netplan-", escaped_netdef_id, NULL);
3420- if (!_netplan_netdef_write_network_file(np_state, def, rootdir, base_config_path, has_been_written, error))
3421- return FALSE;
3422- } else {
3423-diff --git a/src/util.c b/src/util.c
3424-index 86f8c1f..d8477a5 100644
3425---- a/src/util.c
3426-+++ b/src/util.c
3427-@@ -663,17 +663,18 @@ ssize_t
3428- netplan_netdef_get_output_filename(const NetplanNetDefinition* netdef, const char* ssid, char* out_buffer, size_t out_buf_size)
3429- {
3430- g_autofree gchar* conf_path = NULL;
3431-+ g_autofree char* escaped_netdef_id = g_uri_escape_string(netdef->id, NULL, TRUE);
3432-
3433- if (netdef->backend == NETPLAN_BACKEND_NM) {
3434- if (ssid) {
3435- g_autofree char* escaped_ssid = g_uri_escape_string(ssid, NULL, TRUE);
3436-- conf_path = g_strjoin(NULL, "/run/NetworkManager/system-connections/netplan-", netdef->id, "-", escaped_ssid, ".nmconnection", NULL);
3437-+ conf_path = g_strjoin(NULL, "/run/NetworkManager/system-connections/netplan-", escaped_netdef_id, "-", escaped_ssid, ".nmconnection", NULL);
3438- } else {
3439-- conf_path = g_strjoin(NULL, "/run/NetworkManager/system-connections/netplan-", netdef->id, ".nmconnection", NULL);
3440-+ conf_path = g_strjoin(NULL, "/run/NetworkManager/system-connections/netplan-", escaped_netdef_id, ".nmconnection", NULL);
3441- }
3442-
3443- } else if (netdef->backend == NETPLAN_BACKEND_NETWORKD || netdef->backend == NETPLAN_BACKEND_OVS) {
3444-- conf_path = g_strjoin(NULL, "/run/systemd/network/10-netplan-", netdef->id, ".network", NULL);
3445-+ conf_path = g_strjoin(NULL, "/run/systemd/network/10-netplan-", escaped_netdef_id, ".network", NULL);
3446- }
3447-
3448- if (conf_path)
3449-diff --git a/tests/generator/test_common.py b/tests/generator/test_common.py
3450-index 8dfaf7d..f87fef0 100644
3451---- a/tests/generator/test_common.py
3452-+++ b/tests/generator/test_common.py
3453-@@ -822,6 +822,47 @@ UseMTU=true
3454-
3455- self.assert_networkd_udev({'def1.rules': (UDEV_NO_MAC_RULE % ('abc\\"xyz\\n0\\n\\n1', 'eth\\"\\n\\nxyz\\n0'))})
3456-
3457-+ def test_nd_file_paths_escaped(self):
3458-+ self.generate('''network:
3459-+ version: 2
3460-+ ethernets:
3461-+ "abc/../../xyz0":
3462-+ match:
3463-+ driver: "drv"
3464-+ set-name: "eth123"''')
3465-+
3466-+ self.assert_networkd_udev({'abc%2F..%2F..%2Fxyz0.rules': (UDEV_NO_MAC_RULE % ('drv', 'eth123'))})
3467-+ self.assert_networkd({'abc%2F..%2F..%2Fxyz0.network': '''[Match]\nDriver=drv
3468-+Name=eth123
3469-+
3470-+[Network]
3471-+LinkLocalAddressing=ipv6
3472-+''',
3473-+ 'abc%2F..%2F..%2Fxyz0.link': '''[Match]\nDriver=drv\n
3474-+[Link]
3475-+Name=eth123
3476-+WakeOnLan=off
3477-+'''})
3478-+
3479-+ self.generate('''network:
3480-+ version: 2
3481-+ wifis:
3482-+ "abc/../../xyz0":
3483-+ dhcp4: true
3484-+ access-points:
3485-+ "mywifi":
3486-+ password: "aaaaaaaa"''')
3487-+
3488-+ self.assert_wpa_supplicant("abc%2F..%2F..%2Fxyz0", """ctrl_interface=/run/wpa_supplicant
3489-+
3490-+network={
3491-+ ssid=P"mywifi"
3492-+ key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
3493-+ ieee80211w=1
3494-+ psk="aaaaaaaa"
3495-+}
3496-+""")
3497-+
3498-
3499- class TestNetworkManager(TestBase):
3500-
3501-@@ -1349,6 +1390,31 @@ method=ignore
3502- dhcp4: true''', skip_generated_yaml_validation=True)
3503- self.assert_nm_udev(NM_UNMANAGED % 'eth\\n0' + NM_UNMANAGED % 'eth0')
3504-
3505-+ def test_nm_file_paths_escaped(self):
3506-+ self.generate('''network:
3507-+ version: 2
3508-+ renderer: NetworkManager
3509-+ ethernets:
3510-+ "abc/../../xyz0":
3511-+ match:
3512-+ driver: "drv"
3513-+ set-name: "eth123"''')
3514-+
3515-+ self.assert_nm({'abc%2F..%2F..%2Fxyz0': '''[connection]
3516-+id=netplan-abc/../../xyz0
3517-+type=ethernet
3518-+interface-name=eth123
3519-+
3520-+[ethernet]
3521-+wake-on-lan=0
3522-+
3523-+[ipv4]
3524-+method=link-local
3525-+
3526-+[ipv6]
3527-+method=ignore
3528-+'''})
3529-+
3530-
3531- class TestForwardDeclaration(TestBase):
3532-
3533-diff --git a/tests/generator/test_ovs.py b/tests/generator/test_ovs.py
3534-index a6ebbcb..7116705 100644
3535---- a/tests/generator/test_ovs.py
3536-+++ b/tests/generator/test_ovs.py
3537-@@ -1110,3 +1110,27 @@ ExecStart=/usr/bin/ovs-vsctl set Bridge br123 external-ids:netplan/global/set-co
3538- - [portname, portname]
3539- ''', expect_fail=True)
3540- self.assertIn('Open vSwitch patch ports must be of different name', err)
3541-+
3542-+ def test_file_paths_escaped(self):
3543-+ self.generate('''network:
3544-+ version: 2
3545-+ bridges:
3546-+ "abc/../../123":
3547-+ openvswitch: {}
3548-+ vlans:
3549-+ "abc/../../123.100":
3550-+ id: 100
3551-+ link: "abc/../../123"
3552-+''')
3553-+ self.assert_ovs({'abc%2F..%2F..%2F123.service': OVS_BR_EMPTY % {'iface': 'abc/../../123'},
3554-+ 'abc%2F..%2F..%2F123.100.service': OVS_VIRTUAL % {'iface': 'abc/../../123.100', 'extra':
3555-+ '''Requires=netplan-ovs-abc%2F..%2F..%2F123.service
3556-+After=netplan-ovs-abc%2F..%2F..%2F123.service
3557-+
3558-+[Service]
3559-+Type=oneshot
3560-+TimeoutStartSec=10s
3561-+ExecStart=/usr/bin/ovs-vsctl --may-exist add-br abc/../../123.100 abc/../../123 100
3562-+ExecStart=/usr/bin/ovs-vsctl set Interface abc/../../123.100 external-ids:netplan=true
3563-+'''},
3564-+ 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}})
3565diff --git a/debian/patches/lp2066258/0017-backends-escape-semicolons-in-service-units.patch b/debian/patches/lp2066258/0017-backends-escape-semicolons-in-service-units.patch
3566deleted file mode 100644
3567index 008ffad..0000000
3568--- a/debian/patches/lp2066258/0017-backends-escape-semicolons-in-service-units.patch
3569+++ /dev/null
3570@@ -1,386 +0,0 @@
3571-From: Danilo Egea Gondolfo <danilogondolfo@gmail.com>
3572-Date: Fri, 24 May 2024 14:11:12 +0100
3573-Subject: backends: escape semicolons in service units
3574-
3575-Semicolons separated from other words by a combination of spaces and/or
3576-tabs will be escaped.
3577----
3578- src/networkd.c | 10 +++++++
3579- src/openvswitch.c | 3 +++
3580- src/sriov.c | 6 +++++
3581- src/util-internal.h | 3 +++
3582- src/util.c | 34 +++++++++++++++++++++++
3583- tests/ctests/test_netplan_misc.c | 45 +++++++++++++++++++++++++++++++
3584- tests/generator/test_args.py | 54 +++++++++++++++++++++++++++++++++++++
3585- tests/generator/test_ovs.py | 58 ++++++++++++++++++++++++++++++++++++++++
3586- tests/test_sriov.py | 40 +++++++++++++++++++++++++++
3587- 9 files changed, 253 insertions(+)
3588-
3589-diff --git a/src/networkd.c b/src/networkd.c
3590-index 1da684b..1145547 100644
3591---- a/src/networkd.c
3592-+++ b/src/networkd.c
3593-@@ -389,6 +389,9 @@ write_regdom(const NetplanNetDefinition* def, const char* rootdir, GError** erro
3594- g_string_append(s, "\n[Service]\nType=oneshot\n");
3595- g_string_append_printf(s, "ExecStart="SBINDIR"/iw reg set %s\n", def->regulatory_domain);
3596-
3597-+ g_autofree char* new_s = _netplan_scrub_systemd_unit_contents(s->str);
3598-+ g_string_free(s, TRUE);
3599-+ s = g_string_new(new_s);
3600- _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
3601- _netplan_safe_mkdir_p_dir(link);
3602- if (symlink(path, link) < 0 && errno != EEXIST) {
3603-@@ -1275,6 +1278,10 @@ write_wpa_unit(const NetplanNetDefinition* def, const char* rootdir)
3604- } else {
3605- g_string_append(s, " -Dnl80211,wext\n");
3606- }
3607-+
3608-+ g_autofree char* new_s = _netplan_scrub_systemd_unit_contents(s->str);
3609-+ g_string_free(s, TRUE);
3610-+ s = g_string_new(new_s);
3611- _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
3612- }
3613-
3614-@@ -1544,6 +1551,9 @@ _netplan_networkd_write_wait_online(const NetplanState* np_state, const char* ro
3615- }
3616- g_string_append(content, "\n");
3617-
3618-+ g_autofree char* new_content = _netplan_scrub_systemd_unit_contents(content->str);
3619-+ g_string_free(content, TRUE);
3620-+ content = g_string_new(new_content);
3621- _netplan_g_string_free_to_file_with_permissions(content, rootdir, override, NULL, "root", "root", 0640);
3622- g_hash_table_destroy(non_optional_interfaces);
3623- return TRUE;
3624-diff --git a/src/openvswitch.c b/src/openvswitch.c
3625-index 0a81a59..4723230 100644
3626---- a/src/openvswitch.c
3627-+++ b/src/openvswitch.c
3628-@@ -66,6 +66,9 @@ write_ovs_systemd_unit(const char* id, const GString* cmds, const char* rootdir,
3629- g_string_append(s, "StartLimitBurst=0\n");
3630- g_string_append(s, cmds->str);
3631-
3632-+ g_autofree char* new_s = _netplan_scrub_systemd_unit_contents(s->str);
3633-+ g_string_free(s, TRUE);
3634-+ s = g_string_new(new_s);
3635- _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
3636-
3637- _netplan_safe_mkdir_p_dir(link);
3638-diff --git a/src/sriov.c b/src/sriov.c
3639-index 213f124..0435731 100644
3640---- a/src/sriov.c
3641-+++ b/src/sriov.c
3642-@@ -54,6 +54,9 @@ write_sriov_rebind_systemd_unit(GHashTable* pfs, const char* rootdir, GError** e
3643- g_string_truncate(interfaces, interfaces->len-1); /* cut trailing whitespace */
3644- g_string_append_printf(s, "ExecStart=" SBINDIR "/netplan rebind --debug %s\n", interfaces->str);
3645-
3646-+ g_autofree char* new_s = _netplan_scrub_systemd_unit_contents(s->str);
3647-+ g_string_free(s, TRUE);
3648-+ s = g_string_new(new_s);
3649- _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
3650- g_string_free(interfaces, TRUE);
3651-
3652-@@ -90,6 +93,9 @@ write_sriov_apply_systemd_unit(GHashTable* pfs, const char* rootdir, GError** er
3653- g_string_append(s, "\n[Service]\nType=oneshot\n");
3654- g_string_append_printf(s, "ExecStart=" SBINDIR "/netplan apply --sriov-only\n");
3655-
3656-+ g_autofree char* new_s = _netplan_scrub_systemd_unit_contents(s->str);
3657-+ g_string_free(s, TRUE);
3658-+ s = g_string_new(new_s);
3659- _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
3660-
3661- _netplan_safe_mkdir_p_dir(link);
3662-diff --git a/src/util-internal.h b/src/util-internal.h
3663-index 3b33c03..6e602e1 100644
3664---- a/src/util-internal.h
3665-+++ b/src/util-internal.h
3666-@@ -188,3 +188,6 @@ _netplan_netdef_pertype_iter_free(struct netdef_pertype_iter* it);
3667-
3668- gchar*
3669- _netplan_scrub_string(const char* content);
3670-+
3671-+gchar*
3672-+_netplan_scrub_systemd_unit_contents(const char* content);
3673-diff --git a/src/util.c b/src/util.c
3674-index d8477a5..2c52e21 100644
3675---- a/src/util.c
3676-+++ b/src/util.c
3677-@@ -1242,3 +1242,37 @@ _netplan_scrub_string(const char* content)
3678-
3679- return g_string_free(s, FALSE);
3680- }
3681-+
3682-+STATIC gboolean
3683-+_is_space_or_tab(char c)
3684-+{
3685-+ return c == ' ' || c == '\t';
3686-+}
3687-+
3688-+char*
3689-+_netplan_scrub_systemd_unit_contents(const char* content)
3690-+{
3691-+ size_t content_len = strlen(content);
3692-+ // Assume a few replacements will happen to reduce reallocation
3693-+ GString* s = g_string_sized_new(content_len + 8);
3694-+
3695-+ // Append the first character of "content" to the result string
3696-+ g_string_append_len(s, content, 1);
3697-+
3698-+ // Walk from the second element to the one before the last looking for isolated semicolons
3699-+ // A semicolon is isolated if it's surrounded by either tabs or spaces
3700-+ const char* p = content + 1;
3701-+ while (p < (content + content_len - 1)) {
3702-+ if (*p == ';' && _is_space_or_tab(*(p - 1)) && _is_space_or_tab(*(p + 1))) {
3703-+ g_string_append_len(s, "\\;", 2);
3704-+ } else {
3705-+ g_string_append_len(s, p, 1);
3706-+ }
3707-+ p++;
3708-+ }
3709-+
3710-+ // Append the last character of "content" to the result string
3711-+ g_string_append_len(s, p, 1);
3712-+
3713-+ return g_string_free(s, FALSE);
3714-+}
3715-diff --git a/tests/ctests/test_netplan_misc.c b/tests/ctests/test_netplan_misc.c
3716-index 9607662..06676da 100644
3717---- a/tests/ctests/test_netplan_misc.c
3718-+++ b/tests/ctests/test_netplan_misc.c
3719-@@ -502,6 +502,50 @@ test_normalize_ip_address(__unused void** state)
3720- assert_string_equal(normalize_ip_address("0.0.0.0/0", AF_INET), "0.0.0.0/0");
3721- }
3722-
3723-+void
3724-+test_scrub_systemd_unit_content(__unused void** state)
3725-+{
3726-+ char* str = ";abc;";
3727-+ char* res = _netplan_scrub_systemd_unit_contents(str);
3728-+ assert_string_equal(res, ";abc;");
3729-+ g_free(res);
3730-+
3731-+ str = ";;;;";
3732-+ res = _netplan_scrub_systemd_unit_contents(str);
3733-+ assert_string_equal(res, ";;;;");
3734-+ g_free(res);
3735-+
3736-+ str = " ;;;; ";
3737-+ res = _netplan_scrub_systemd_unit_contents(str);
3738-+ assert_string_equal(res, " ;;;; ");
3739-+ g_free(res);
3740-+
3741-+ str = "; ; ; ; ;";
3742-+ res = _netplan_scrub_systemd_unit_contents(str);
3743-+ assert_string_equal(res, "; \\; \\; \\; ;");
3744-+ g_free(res);
3745-+
3746-+ str = " ; ; ; ; ; ";
3747-+ res = _netplan_scrub_systemd_unit_contents(str);
3748-+ assert_string_equal(res, " \\; \\; \\; \\; \\; ");
3749-+ g_free(res);
3750-+
3751-+ str = "a ; ; ; ; ; b";
3752-+ res = _netplan_scrub_systemd_unit_contents(str);
3753-+ assert_string_equal(res, "a \\; \\; \\; \\; \\; b");
3754-+ g_free(res);
3755-+
3756-+ str = "\t;\t; ;\t; \t ;\t ";
3757-+ res = _netplan_scrub_systemd_unit_contents(str);
3758-+ assert_string_equal(res, "\t\\;\t\\; \\;\t\\; \t \\;\t ");
3759-+ g_free(res);
3760-+
3761-+ str = "\t;\t;\t;\t;\t;\t";
3762-+ res = _netplan_scrub_systemd_unit_contents(str);
3763-+ assert_string_equal(res, "\t\\;\t\\;\t\\;\t\\;\t\\;\t");
3764-+ g_free(res);
3765-+}
3766-+
3767- int
3768- setup(__unused void** state)
3769- {
3770-@@ -539,6 +583,7 @@ main()
3771- cmocka_unit_test(test_normalize_ip_address),
3772- cmocka_unit_test(test_util_get_link_local_true),
3773- cmocka_unit_test(test_util_get_link_local_false),
3774-+ cmocka_unit_test(test_scrub_systemd_unit_content),
3775- };
3776-
3777- return cmocka_run_group_tests(tests, setup, tear_down);
3778-diff --git a/tests/generator/test_args.py b/tests/generator/test_args.py
3779-index c2d0d98..45f51d0 100644
3780---- a/tests/generator/test_args.py
3781-+++ b/tests/generator/test_args.py
3782-@@ -260,3 +260,57 @@ ConditionPathIsSymbolicLink=/run/systemd/generator/network-online.target.wants/s
3783- except subprocess.CalledProcessError as e:
3784- self.assertEqual(e.returncode, 1)
3785- self.assertIn(b'can not be called directly', e.output)
3786-+
3787-+ def test_systemd_generator_escaping(self):
3788-+ conf = os.path.join(self.confdir, 'a.yaml')
3789-+ os.makedirs(os.path.dirname(conf))
3790-+ with open(conf, 'w') as f:
3791-+ f.write('''network:
3792-+ version: 2
3793-+ ethernets:
3794-+ lo:
3795-+ match:
3796-+ name: lo
3797-+ set-name: "a ; b\\t; c\\t; d \\n 123 ; echo "
3798-+ addresses: ["127.0.0.1/8", "::1/128"]''')
3799-+ os.chmod(conf, mode=0o600)
3800-+ outdir = os.path.join(self.workdir.name, 'out')
3801-+ os.mkdir(outdir)
3802-+
3803-+ generator = os.path.join(self.workdir.name, 'systemd', 'system-generators', 'netplan')
3804-+ os.makedirs(os.path.dirname(generator))
3805-+ os.symlink(exe_generate, generator)
3806-+
3807-+ subprocess.check_call([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir])
3808-+ n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-lo.network')
3809-+ self.assertTrue(os.path.exists(n))
3810-+ os.unlink(n)
3811-+
3812-+ # should auto-enable networkd and -wait-online
3813-+ service_dir = os.path.join(self.workdir.name, 'run', 'systemd', 'system')
3814-+ self.assertTrue(os.path.islink(os.path.join(
3815-+ outdir, 'multi-user.target.wants', 'systemd-networkd.service')))
3816-+ self.assertTrue(os.path.islink(os.path.join(
3817-+ outdir, 'network-online.target.wants', 'systemd-networkd-wait-online.service')))
3818-+ override = os.path.join(service_dir, 'systemd-networkd-wait-online.service.d', '10-netplan.conf')
3819-+ self.assertTrue(os.path.isfile(override))
3820-+ with open(override, 'r') as f:
3821-+ # eth99 does not exist on the system, so will not be listed
3822-+ self.assertEqual(f.read(), '''[Unit]
3823-+ConditionPathIsSymbolicLink=/run/systemd/generator/network-online.target.wants/systemd-networkd-wait-online.service
3824-+
3825-+[Service]
3826-+ExecStart=
3827-+ExecStart=/lib/systemd/systemd-networkd-wait-online -i a \\; b\\t; c\\t; d \\n 123 \\; echo :degraded\n''')
3828-+
3829-+ # should be a no-op the second time while the stamp exists
3830-+ out = subprocess.check_output([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir],
3831-+ stderr=subprocess.STDOUT)
3832-+ self.assertFalse(os.path.exists(n))
3833-+ self.assertIn(b'netplan generate already ran', out)
3834-+
3835-+ # after removing the stamp it generates again, and not trip over the
3836-+ # existing enablement symlink
3837-+ os.unlink(os.path.join(outdir, 'netplan.stamp'))
3838-+ subprocess.check_output([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir])
3839-+ self.assertTrue(os.path.exists(n))
3840-diff --git a/tests/generator/test_ovs.py b/tests/generator/test_ovs.py
3841-index 7116705..4393cea 100644
3842---- a/tests/generator/test_ovs.py
3843-+++ b/tests/generator/test_ovs.py
3844-@@ -1134,3 +1134,61 @@ ExecStart=/usr/bin/ovs-vsctl --may-exist add-br abc/../../123.100 abc/../../123
3845- ExecStart=/usr/bin/ovs-vsctl set Interface abc/../../123.100 external-ids:netplan=true
3846- '''},
3847- 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}})
3848-+
3849-+ def test_control_characters_and_semicolons_escaping(self):
3850-+ self.generate('''network:
3851-+ version: 2
3852-+ bridges: # bridges first, to trigger multi-pass processing
3853-+ ovs0:
3854-+ interfaces: [eth0, eth1]
3855-+ openvswitch: {}
3856-+ ethernets:
3857-+ eth0:
3858-+ openvswitch:
3859-+ external-ids:
3860-+ "a\\n1\\ra": " ; a ; 1 ;a; ;b\\t;\\t3 ;\\ta\\t; 1"
3861-+ other-config:
3862-+ "a\\n1\\ra": " ; a ; 1 ;a; ;b\\t;\\t3 ;\\ta\\t; 1"
3863-+ dhcp6: true
3864-+ eth1:
3865-+ dhcp4: true
3866-+ openvswitch:
3867-+ other-config:
3868-+ disable-in-band: false\n''', skip_generated_yaml_validation=True)
3869-+ self.assert_ovs({'ovs0.service': OVS_VIRTUAL % {'iface': 'ovs0', 'extra': '''
3870-+[Service]
3871-+Type=oneshot
3872-+TimeoutStartSec=10s
3873-+ExecStart=/usr/bin/ovs-vsctl --may-exist add-br ovs0
3874-+ExecStart=/usr/bin/ovs-vsctl --may-exist add-port ovs0 eth1
3875-+ExecStart=/usr/bin/ovs-vsctl --may-exist add-port ovs0 eth0
3876-+''' + OVS_BR_DEFAULT % {'iface': 'ovs0'}},
3877-+ 'eth0.service': OVS_PHYSICAL % {'iface': 'eth0', 'extra': '''\
3878-+Requires=netplan-ovs-ovs0.service
3879-+After=netplan-ovs-ovs0.service
3880-+
3881-+[Service]
3882-+Type=oneshot
3883-+TimeoutStartSec=10s
3884-+ExecStart=/usr/bin/ovs-vsctl set Interface eth0 external-ids:a\\n1\\ra= \\; a \\; 1 ;a; ;b\\t;\\t3 ;\\ta\\t; 1
3885-+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
3886-+ExecStart=/usr/bin/ovs-vsctl set Interface eth0 other-config:a\\n1\\ra= \\; a \\; 1 ;a; ;b\\t;\\t3 ;\\ta\\t; 1
3887-+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
3888-+'''},
3889-+ 'eth1.service': OVS_PHYSICAL % {'iface': 'eth1', 'extra': '''\
3890-+Requires=netplan-ovs-ovs0.service
3891-+After=netplan-ovs-ovs0.service
3892-+
3893-+[Service]
3894-+Type=oneshot
3895-+TimeoutStartSec=10s
3896-+ExecStart=/usr/bin/ovs-vsctl set Interface eth1 other-config:disable-in-band=false
3897-+ExecStart=/usr/bin/ovs-vsctl set Interface eth1 external-ids:netplan/other-config/disable-in-band=false
3898-+'''},
3899-+ 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}})
3900-+ # Confirm that the networkd config is still sane
3901-+ self.assert_networkd({'ovs0.network': ND_EMPTY % ('ovs0', 'ipv6'),
3902-+ 'eth0.network': (ND_DHCP6 % 'eth0')
3903-+ .replace('LinkLocalAddressing=ipv6', 'LinkLocalAddressing=no\nBridge=ovs0'),
3904-+ 'eth1.network': (ND_DHCP4 % 'eth1')
3905-+ .replace('LinkLocalAddressing=ipv6', 'LinkLocalAddressing=no\nBridge=ovs0')})
3906-diff --git a/tests/test_sriov.py b/tests/test_sriov.py
3907-index e842396..a7102fb 100644
3908---- a/tests/test_sriov.py
3909-+++ b/tests/test_sriov.py
3910-@@ -1172,6 +1172,46 @@ Before=network-pre.target
3911- After=sys-subsystem-net-devices-enblue.device
3912- After=sys-subsystem-net-devices-engreen.device
3913-
3914-+[Service]
3915-+Type=oneshot
3916-+ExecStart=/usr/sbin/netplan apply --sriov-only
3917-+'''})
3918-+
3919-+ def test_escaping_semicolons_from_unit_file(self):
3920-+ ''' Check if semicolons and line breaks are properly escaped in the generated
3921-+ systemd service unit.
3922-+ '''
3923-+ self.generate('''network:
3924-+ version: 2
3925-+ ethernets:
3926-+ engreen:
3927-+ embedded-switch-mode: switchdev
3928-+ delay-virtual-functions-rebind: true
3929-+ enblue:
3930-+ match: {driver: dummy_driver}
3931-+ set-name: ";en ; a\\t;\\tb ;\\tc\\t; d; \\n;\\nabc"
3932-+ embedded-switch-mode: legacy
3933-+ delay-virtual-functions-rebind: true
3934-+ virtual-function-count: 4
3935-+ sriov_vf0:
3936-+ link: engreen''', skip_generated_yaml_validation=True)
3937-+ self.assert_sriov({'rebind.service': '''[Unit]
3938-+Description=(Re-)bind SR-IOV Virtual Functions to their driver
3939-+After=network.target
3940-+After=netplan-sriov-apply.service
3941-+After=sys-subsystem-net-devices-;en \\; a\\t;\\tb ;\\tc\\t; d; \\n;\\nabc.device
3942-+After=sys-subsystem-net-devices-engreen.device
3943-+
3944-+[Service]
3945-+Type=oneshot
3946-+ExecStart=/usr/sbin/netplan rebind --debug ;en \\; a\\t;\\tb ;\\tc\\t; d; \\n;\\nabc engreen
3947-+''', 'apply.service': '''[Unit]
3948-+Description=Apply SR-IOV configuration
3949-+DefaultDependencies=no
3950-+Before=network-pre.target
3951-+After=sys-subsystem-net-devices-;en \\; a\\t;\\tb ;\\tc\\t; d; \\n;\\nabc.device
3952-+After=sys-subsystem-net-devices-engreen.device
3953-+
3954- [Service]
3955- Type=oneshot
3956- ExecStart=/usr/sbin/netplan apply --sriov-only
3957diff --git a/debian/patches/series b/debian/patches/series
3958index c40a500..63aae32 100644
3959--- a/debian/patches/series
3960+++ b/debian/patches/series
3961@@ -1,17 +1,4 @@
3962 python-limited-stable-api.patch
3963-0002-parse-nm-add-a-workaround-for-the-DoT-DNS-option.patch
3964-lp2060311/0003-util-fix-potential-NULL-pointer-assert.patch
3965-lp2060311/0004-networkd-add-wait-online-enumeration-utils.patch
3966-lp2060311/0005-generate-enable-systemd-networkd-wait-online-for-non.patch
3967-lp2060311/0006-CLI-utils-Do-not-ask-for-daemon-reload-password-inte.patch
3968-lp2060311/0007-CLI-generate-call-daemon-reload-after-re-generating-.patch
3969-lp2060311/0008-wait-online-Do-not-block-on-loopback-interface.patch
3970-lp2060311/0009-generate-Do-not-touch-wait-online-if-we-don-t-have-a.patch
3971-lp2060311/0010-wait-online-wait-for-existing-interfaces-only-and-do.patch
3972-lp2060311/0011-wait-online-account-for-DHCPv4-v6-addresses.patch
3973-lp2060311/0012-wait-online-do-not-require-virtual-devices-to-be-cre.patch
3974-lp2060311/0013-wait-online-recognize-that-bridge-bond-members-will-.patch
3975-lp2065738/0014-libnetplan-use-more-restrictive-file-permissions.patch
3976-lp2066258/0015-libnetplan-escape-control-characters.patch
3977-lp2066258/0016-backends-escape-file-paths.patch
3978-lp2066258/0017-backends-escape-semicolons-in-service-units.patch
3979+0002-CLI-apply-call-udevadm-trigger-using-action-add-Clos.patch
3980+0003-Revert-wait-online-disabled-wait-online-for-stable-1.patch
3981+0004-generate-avoid-calling-udevadm-control-reload-LP-199.patch
3982diff --git a/debian/tests/control b/debian/tests/control
3983index d7af917..7852549 100644
3984--- a/debian/tests/control
3985+++ b/debian/tests/control
3986@@ -195,7 +195,7 @@ Depends: @,
3987 libnm0,
3988 python3-gi,
3989 gir1.2-nm-1.0,
3990-Restrictions: allow-stderr, needs-root, isolation-container, flaky
3991+Restrictions: allow-stderr, needs-root, isolation-container
3992 Features: test-name=diff
3993
3994 Test-Command: ./debian/tests/prep-testbed.sh && ./debian/tests/autostart.sh
3995diff --git a/doc/.sphinx/requirements.txt b/doc/.sphinx/requirements.txt
3996index bc73a32..e9c2a91 100644
3997--- a/doc/.sphinx/requirements.txt
3998+++ b/doc/.sphinx/requirements.txt
3999@@ -14,3 +14,4 @@ sphinx-reredirects
4000 sphinx-tabs
4001 sphinxcontrib-jquery
4002 sphinxext-opengraph
4003+watchfiles
4004diff --git a/doc/conf.py b/doc/conf.py
4005index 0e638c0..3f60865 100644
4006--- a/doc/conf.py
4007+++ b/doc/conf.py
4008@@ -159,7 +159,6 @@ custom_excludes = [
4009 'doc-cheat-sheet*',
4010 'manpage-footer.md',
4011 'manpage-header.md',
4012- 'CODE_OF_CONDUCT.md',
4013 ]
4014
4015 # Add CSS files (located in .sphinx/_static/)
4016diff --git a/doc/creating-link-aggregation.md b/doc/creating-link-aggregation.md
4017new file mode 100644
4018index 0000000..2543d00
4019--- /dev/null
4020+++ b/doc/creating-link-aggregation.md
4021@@ -0,0 +1,86 @@
4022+# How to create link aggregation
4023+
4024+:::{note}
4025+These instructions assume a system setup based on the example configuration outlined in the [Netplan tutorial](/netplan-tutorial).
4026+:::
4027+
4028+Let's suppose now that you need to configure your system to connect to your
4029+ISP links via a link aggregation. On Linux you can do that with a `bond`
4030+virtual interface.
4031+
4032+On Netplan, an interface of type `bond` can be created inside a `bonds` mapping.
4033+
4034+Now that the traffic will flow through the link aggregation, you will move
4035+all the addressing configuration to the bond itself.
4036+
4037+You can define a list of interfaces that will be attached to the bond. In our
4038+simple scenario, we have a single one.
4039+
4040+Edit the file `/etc/netplan/second-interface.yaml` and make the following changes:
4041+
4042+```yaml
4043+network:
4044+ version: 2
4045+ ethernets:
4046+ netplan-isp-interface:
4047+ dhcp4: false
4048+ dhcp6: false
4049+ match:
4050+ macaddress: 00:16:3e:0c:97:8a
4051+ set-name: netplan-isp
4052+ bonds:
4053+ isp-bond0:
4054+ interfaces:
4055+ - netplan-isp-interface
4056+ dhcp4: false
4057+ dhcp6: false
4058+ accept-ra: false
4059+ link-local: []
4060+ addresses:
4061+ - 172.16.0.1/24
4062+ routes:
4063+ - to: default
4064+ via: 172.16.0.254
4065+ nameservers:
4066+ search:
4067+ - netplanlab.local
4068+ addresses:
4069+ - 172.16.0.254
4070+ - 172.16.0.253
4071+```
4072+
4073+Note that you can reference the interface used in the bond by the name you
4074+defined for it in the `ethernets` section.
4075+
4076+Now use `netplan apply` to apply your changes
4077+
4078+```
4079+netplan apply
4080+```
4081+
4082+Now your system has a new interface called `isp-bond0`. Use the command
4083+`ip address show isp-bond0` or `netplan status` to check its state:
4084+
4085+```
4086+netplan status isp-bond0
4087+```
4088+
4089+You should see an output similar to the one below:
4090+
4091+```
4092+ Online state: online
4093+ DNS Addresses: 127.0.0.53 (stub)
4094+ DNS Search: lxd
4095+ netplanlab.local
4096+
4097+● 4: isp-bond0 bond UP (networkd: isp-bond0)
4098+ MAC Address: b2:6b:19:b1:9a:86
4099+ Addresses: 172.16.0.1/24
4100+ DNS Addresses: 172.16.0.254
4101+ 172.16.0.253
4102+ DNS Search: netplanlab.local
4103+ Routes: default via 172.16.0.254 (static)
4104+ 172.16.0.0/24 from 172.16.0.1 (link)
4105+
4106+3 inactive interfaces hidden. Use "--all" to show all.
4107+```
4108diff --git a/doc/howto.md b/doc/howto.md
4109index 8385090..326ec11 100644
4110--- a/doc/howto.md
4111+++ b/doc/howto.md
4112@@ -1,28 +1,40 @@
4113 # How-to guides
4114
4115-Below is a collection of how-to guides for common scenarios.
4116-If you see a scenario missing or have one to contribute, please,
4117-[file a bug](https://bugs.launchpad.net/netplan/+filebug) against this
4118-documentation with the example.
4119+This is a collection of how-to guides for common scenarios. If you see a scenario missing or have one to contribute, [file an issue](https://bugs.launchpad.net/netplan/+filebug) against this documentation with the example.
4120
4121-To configure Netplan, save configuration files in the `/etc/netplan/` directory
4122-with a `.yaml` extension (e.g. `/etc/netplan/config.yaml`), then run
4123-`sudo netplan apply`. This command parses and applies the configuration to the
4124-system. Configuration written to disk under `/etc/netplan/` persists between
4125-reboots.
4126+To configure Netplan, save configuration files in the `/etc/netplan/` directory with a `.yaml` extension (e.g. `/etc/netplan/config.yaml`), then run `sudo netplan apply`. This command parses and applies the configuration to the system. Configuration written to disk under `/etc/netplan/` persists between reboots. Visit [Applying new Netplan configuration](/netplan-tutorial.md#applying-new-netplan-configuration) for detailed guidance.
4127
4128-For each of the example below, use the `renderer` that applies to your scenario.
4129-For example, for Ubuntu Desktop the `renderer` is usually `NetworkManager`,
4130-and `networkd` for Ubuntu Server.
4131+For each of the examples below, use the `renderer` that applies to your scenario. For example, for Ubuntu Desktop, the `renderer` is usually `NetworkManager`. For Ubuntu Server, it is `networkd`.
4132
4133-Also, see [/examples](https://github.com/canonical/netplan/tree/main/examples)
4134-on GitHub.
4135+
4136+## Quick configuration examples
4137
4138 ```{toctree}
4139 :maxdepth: 1
4140
4141 examples
4142+```
4143+
4144+YAML configuration snippets for these examples, as well as additional examples, are available in the [examples](https://github.com/canonical/netplan/tree/main/examples) directory on GitHub.
4145+
4146+
4147+## Complex how-to guides
4148+
4149+```{toctree}
4150+:maxdepth: 1
4151+
4152+using-static-ip-addresses
4153+matching-interface-by-mac-address
4154+creating-link-aggregation
4155 dbus-config
4156 netplan-everywhere
4157+```
4158+
4159+
4160+## Documentation
4161+
4162+```{toctree}
4163+:maxdepth: 1
4164+
4165 contribute-docs
4166 ```
4167diff --git a/doc/matching-interface-by-mac-address.md b/doc/matching-interface-by-mac-address.md
4168new file mode 100644
4169index 0000000..916cef3
4170--- /dev/null
4171+++ b/doc/matching-interface-by-mac-address.md
4172@@ -0,0 +1,85 @@
4173+# How to match the interface by MAC address
4174+
4175+:::{note}
4176+These instructions assume a system setup based on the example configuration outlined in the [Netplan tutorial](/netplan-tutorial).
4177+:::
4178+
4179+Sometimes you can't rely on the interface names to apply configuration to them. Changes in the system might cause a change in their names, such as when you move an interface card from a PCI slot to another.
4180+
4181+In this exercise you will use the `match` keyword to locate the device based on its MAC address and also set a more meaningful name to the interface.
4182+
4183+Let's assume that your second interface is connected to the Netplan ISP internet provider company and you want to identify it as such.
4184+
4185+First identify its MAC address:
4186+
4187+```
4188+ip link show enp6s0
4189+```
4190+
4191+In the output, the MAC address is the number in front of the `link/ether` property.
4192+```
4193+3: enp6s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
4194+ link/ether 00:16:3e:0c:97:8a brd ff:ff:ff:ff:ff:ff
4195+```
4196+
4197+Edit the file `/etc/netplan/second-interface.yaml` and make the following changes:
4198+
4199+```yaml
4200+network:
4201+ version: 2
4202+ ethernets:
4203+ netplan-isp-interface:
4204+ match:
4205+ macaddress: 00:16:3e:0c:97:8a
4206+ set-name: netplan-isp
4207+ dhcp4: false
4208+ dhcp6: false
4209+ accept-ra: false
4210+ link-local: []
4211+ addresses:
4212+ - 172.16.0.1/24
4213+ routes:
4214+ - to: default
4215+ via: 172.16.0.254
4216+ nameservers:
4217+ search:
4218+ - netplanlab.local
4219+ addresses:
4220+ - 172.16.0.254
4221+ - 172.16.0.253
4222+```
4223+
4224+These are the important changes in this exercise:
4225+
4226+```yaml
4227+ ethernets:
4228+ netplan-isp-interface:
4229+ match:
4230+ macaddress: 00:16:3e:0c:97:8a
4231+ set-name: netplan-isp
4232+```
4233+
4234+Note that, as you are now matching the interface by its MAC address, you are free to identify it with a different name. It makes it easier to read and find information in the YAML file.
4235+
4236+After changing the file, apply your new configuration:
4237+
4238+```
4239+netplan apply
4240+```
4241+
4242+Now list your interfaces:
4243+
4244+```
4245+ip link show
4246+```
4247+
4248+As you can see, your interface is now called `netplan-isp`.
4249+
4250+```
4251+1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
4252+ link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
4253+2: enp5s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
4254+ link/ether 00:16:3e:13:ae:10 brd ff:ff:ff:ff:ff:ff
4255+3: netplan-isp: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
4256+ link/ether 00:16:3e:0c:97:8a brd ff:ff:ff:ff:ff:ff
4257+```
4258diff --git a/doc/netplan-generate.md b/doc/netplan-generate.md
4259index 3c0d415..c2675d0 100644
4260--- a/doc/netplan-generate.md
4261+++ b/doc/netplan-generate.md
4262@@ -58,8 +58,8 @@ There are 3 locations that **`netplan generate`** considers:
4263 * `/run/netplan/*.yaml`
4264
4265 If there are multiple files with exactly the same name, then only one
4266-will be read. A file in `/run/netplan` will shadow - completely replace
4267-- a file with the same name in `/etc/netplan`. A file in `/etc/netplan`
4268+will be read. A file in `/run/netplan` will shadow (completely replace)
4269+a file with the same name in `/etc/netplan`. A file in `/etc/netplan`
4270 will itself shadow a file in `/lib/netplan`.
4271
4272 Or, in other words, `/run/netplan` is top priority, then `/etc/netplan`,
4273diff --git a/doc/netplan-status.md b/doc/netplan-status.md
4274index c4ad453..374ce23 100644
4275--- a/doc/netplan-status.md
4276+++ b/doc/netplan-status.md
4277@@ -35,10 +35,10 @@ Currently, **`netplan status`** depends on `systemd-networkd` as a source of dat
4278 : Show all interface data including inactive.
4279
4280 `--diff`
4281-: Analyze and display differences between the current system configuration and network definitions present in the YAML files.
4282- The configuration analyzed includes IP addresses, routes, MAC addresses, DNS addresses, search domains and missing network interfaces.
4283+: Analyse and display differences between the current system configuration and network definitions present in the YAML files.
4284+ The configuration analysed includes IP addresses, routes, MAC addresses, DNS addresses, search domains and missing network interfaces.
4285
4286- The output format is similar to popular diff tools, such `diff` and `git diff`. Configuration present only in the system (and therefore missing in the Netplan YAMLs)
4287+ The output format is similar to popular diff tools, such `diff` and `git diff`. Configuration present only in the system (and therefore missing in the Netplan YAML)
4288 will be displayed with a `+` sign and will be highlighted in green. Configuration present only in Netplan (and therefore missing in the system) will be displayed
4289 with a `-` sign and highlighted in red. The same is applied to network interfaces.
4290
4291diff --git a/doc/netplan-tutorial.md b/doc/netplan-tutorial.md
4292index ef4ebbc..344096f 100644
4293--- a/doc/netplan-tutorial.md
4294+++ b/doc/netplan-tutorial.md
4295@@ -1,108 +1,119 @@
4296-# Pre-requisites
4297+# Netplan tutorial
4298
4299-In order to do the exercises yourself you will need a virtual machine, preferably running Ubuntu. In this tutorial, we will use LXD to create virtual networks and launch virtual machines. Feel free to use a cloud instance or a different hypervisor. As long as you can achieve the same results, you should be fine. If you're going to use your own desktop/laptop system, some of the exercises might interrupt your network connectivity.
4300+Use this tutorial to learn about basic use of the Netplan utility: how to set up an environment to try it, run it for the first time, check its configuration, and modify its settings.
4301
4302-If you already have a setup where you can do the exercises you can just skip this section.
4303
4304-## Setting up the environment
4305+# Trying Netplan in a virtual machine
4306
4307-You can follow the steps below to install and create a basic LXD configuration you can use to launch virtual machines.
4308+To try Netplan, you can use a virtual environment, preferably running Ubuntu. This tutorial uses LXD to create virtual networks and launch virtual machines. You can also use a cloud instance or a different hypervisor.
4309
4310-For more information about LXD, please visit [linuxcontainers.org](https://documentation.ubuntu.com/lxd/).
4311+:::{warning}
4312+When using your own system without virtualisation, some of the exercises might interrupt your network connectivity.
4313+:::
4314
4315-First, install LXD: [LXD | How to install LXD](https://documentation.ubuntu.com/lxd/en/latest/installing/)
4316
4317-On Ubuntu, you can install it using `snap`:
4318+## Setting up the virtual environment
4319
4320-```
4321-snap install lxd
4322-```
4323+Follow the steps below to install and create a basic LXD configuration to launch virtual machines (VM). For more information about LXD, visit [documentation.ubuntu.com/lxd](https://documentation.ubuntu.com/lxd/).
4324
4325-Now, initialise your LXD configuration:
4326+1. Install LXD: [LXD | How to install LXD](https://documentation.ubuntu.com/lxd/en/latest/installing/).
4327
4328-```
4329-lxd init --minimal
4330-```
4331+ On Ubuntu, use `snap` to install LXD:
4332
4333-Run the command below to create a new network in LXD. For some of the exercises you will need a second network interface in your virtual machine.
4334+ ```
4335+ snap install lxd
4336+ ```
4337
4338-```
4339-lxc network create netplanbr0 --type=bridge
4340-```
4341+2. Initialise LXD configuration:
4342
4343-You should see the output below:
4344+ ```
4345+ lxd init --minimal
4346+ ```
4347
4348-```
4349-Network netplanbr0 created
4350-```
4351+3. Create a new network in LXD (some of the exercises require a second network interface in the virtual machine):
4352
4353-At this point you should have a usable LXD installation with a working network bridge.
4354+ ```
4355+ lxc network create netplanbr0 --type=bridge
4356+ ```
4357
4358-Now create a virtual machine called `netplan-lab0`:
4359+ The following output confirms the successful creation of the bridge network:
4360
4361-```
4362-lxc init --vm ubuntu:22.04 netplan-lab0
4363-```
4364+ ```
4365+ Network netplanbr0 created
4366+ ```
4367
4368-You should see the output below:
4369+ Now you have a usable LXD installation with a working network bridge.
4370
4371-```
4372-Creating netplan-lab0
4373-```
4374+4. Create a virtual machine called `netplan-lab0`:
4375
4376-The new VM will have one network interface attached to the default LXD bridge.
4377+ ```none
4378+ lxc init --vm ubuntu:23.10 netplan-lab0
4379+ ```
4380
4381-Now attach the network you created just now, `netplanbr0`, to your VM, `netplan-lab0`, as interface `eth1`:
4382+ You should see the output below:
4383
4384-```
4385-lxc network attach netplanbr0 netplan-lab0 eth1
4386-```
4387+ ```
4388+ Creating netplan-lab0
4389+ ```
4390
4391-> See more: [LXD | Attach a network to an instance](https://documentation.ubuntu.com/lxd/en/latest/howto/network_create/#attach-a-network-to-an-instance)
4392+ The new VM has one network interface attached to the default LXD bridge.
4393
4394-And start your new VM:
4395+5. Attach the network you created (`netplanbr0`) to the VM (`netplan-lab0`) as the `eth1` interface:
4396
4397-```
4398-lxc start netplan-lab0
4399-```
4400+ ```
4401+ lxc network attach netplanbr0 netplan-lab0 eth1
4402+ ```
4403
4404-Access your new VM using `lxc shell`:
4405+ :::{tip}
4406+ For more on LXD networking, visit [LXD | Attach a network to an instance](https://documentation.ubuntu.com/lxd/en/latest/howto/network_create/#attach-a-network-to-an-instance).
4407+ :::
4408
4409-```
4410-lxc shell netplan-lab0
4411-```
4412+6. Start the new VM:
4413
4414-> **If this doesn't work for you**: Try instead `lxc exec netplan-lab0 bash` or `lxc console netplan-lab0`.
4415+ ```
4416+ lxc start netplan-lab0
4417+ ```
4418
4419-You should now have a root shell inside your VM:
4420+7. Access the new VM using `lxc shell`:
4421
4422-```
4423-root@netplan-lab0:~#
4424-```
4425+ ```
4426+ lxc shell netplan-lab0
4427+ ```
4428
4429-Run the command `ip link` to show your network interfaces:
4430+ In case of problems, try running `lxc exec netplan-lab0 bash` or `lxc console netplan-lab0`.
4431
4432-```
4433-ip link
4434-```
4435+ You should now have a root shell inside the VM:
4436
4437-You should see an output similar to the below:
4438+ ```none
4439+ root@netplan-lab0:~#
4440+ ```
4441
4442-```
4443-1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
4444- link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
4445-2: enp5s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
4446- link/ether 00:16:3e:13:ae:10 brd ff:ff:ff:ff:ff:ff
4447-3: enp6s0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
4448- link/ether 00:16:3e:0c:97:8a brd ff:ff:ff:ff:ff:ff
4449-```
4450+8. Run the `ip link` command to show the network interfaces:
4451+
4452+ ```
4453+ ip link
4454+ ```
4455+
4456+ You should see an output similar to the below:
4457+
4458+ ```none
4459+ 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
4460+ link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
4461+ 2: enp5s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
4462+ link/ether 00:16:3e:13:ae:10 brd ff:ff:ff:ff:ff:ff
4463+ 3: enp6s0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
4464+ link/ether 00:16:3e:0c:97:8a brd ff:ff:ff:ff:ff:ff
4465+ ```
4466
4467-In this case, `enp5s0` is the primary interface connected to the default LXD network and `enp6s0` is the second interface you added connected to your custom network.
4468+ In this case:
4469
4470+ * `enp5s0` is the primary interface connected to the default LXD network.
4471+ * `enp6s0` is the second interface you added connected to your custom network.
4472
4473-Now, let's start with a simple exercise.
4474+You're ready to start the first exercise.
4475
4476-# Running netplan for the first time
4477+
4478+# Running Netplan for the first time
4479
4480 Start by typing the command `netplan` in your shell:
4481
4482@@ -110,9 +121,9 @@ Start by typing the command `netplan` in your shell:
4483 netplan
4484 ```
4485
4486-You should see the output below
4487+You should see the following output:
4488
4489-```
4490+```none
4491 You need to specify a command
4492 usage: /usr/sbin/netplan [-h] [--debug] ...
4493
4494@@ -136,11 +147,12 @@ Available commands:
4495 try Try to apply a new Netplan config to running system, with automatic rollback
4496 ```
4497
4498-As you can see, Netplan has a number of sub commands. Let's explore some of them.
4499+As you can see, Netplan has a number of sub-commands. Let's explore some of them.
4500+
4501
4502-# Showing your current Netplan configuration
4503+# Showing current Netplan configuration
4504
4505-To show your current configuration, run the command `netplan get`.
4506+To show the current configuration, run the `netplan get` command:
4507
4508 ```
4509 netplan get
4510@@ -148,7 +160,6 @@ netplan get
4511
4512 You should see an output similar to the one below:
4513
4514-
4515 ```yaml
4516 network:
4517 version: 2
4518@@ -157,16 +168,17 @@ network:
4519 dhcp4: true
4520 ```
4521
4522-It shows you have an Ethernet interface called `enp5s0` and it has DHCP enabled for IPv4.
4523+This means:
4524
4525+* There's an Ethernet interface called `enp5s0`.
4526+* DHCP is enabled for the IPv4 protocol on `enp5s0`.
4527
4528-# Showing your current network configuration
4529
4530-Netplan 0.106 introduced the `netplan status` command. You can use it to
4531-show your system's current network configuration. Try that by typing
4532-`netplan status --all` in your console:
4533+# Showing current network configuration
4534
4535-```
4536+Netplan 0.106 introduced the `netplan status` command. The command displays the current network configuration of the system. Try it by running:
4537+
4538+```none
4539 netplan status --all
4540 ```
4541
4542@@ -202,11 +214,18 @@ You should see an output similar to the one below:
4543 MAC Address: 00:16:3e:0c:97:8a (Red Hat, Inc.)
4544 ```
4545
4546-# Checking the file where your configuration is stored
4547
4548-The configuration you just listed is stored at `/etc/netplan`. You can see the contents of the file with the command below:
4549+# Checking Netplan configuration files
4550+
4551+Netplan configuration is stored in YAML-formatted files in the `/etc/netplan` directory. To display the contents of the directory, run:
4552
4553+```none
4554+ls -1 /etc/netplan/
4555 ```
4556+
4557+Provided your system was initialised using `cloud-init`, such as the Ubuntu virtual machine recommended for testing Netplan in [Trying Netplan in a virtual machine](#trying-netplan-in-a-virtual-machine), you can find the initial Netplan configuration in the `50-cloud-init.yaml` file:
4558+
4559+```none
4560 cat /etc/netplan/50-cloud-init.yaml
4561 ```
4562
4563@@ -225,518 +244,205 @@ network:
4564 dhcp4: true
4565 ```
4566
4567-This file was automatically generated by `cloud-init` when the system was initialised. As noted in the comments, changes to this file will not persist.
4568+This configuration file is automatically generated by the `cloud-init` tool when the system is initialised. As noted in the file comments, direct changes to this file do not persist.
4569
4570-# Enabling your second network interface with DHCP
4571
4572-There are basically 2 ways to create or change Netplan configuration:
4573+# Creating and modifying Netplan configuration
4574
4575-1) Using the `netplan set` command
4576-2) Editing the YAML files manually
4577+There are two methods to create or modify Netplan configuration:
4578
4579-Let's see how you can enable your second network interface using both ways.
4580+Using the `netplan set` command
4581+: Example: [Using netplan set to enable a network interface with DHCP](#using-netplan-set-to-enable-a-network-interface-with-dhcp)
4582
4583-## Using `netplan set`
4584+Editing the YAML configuration files manually
4585+: Example: [Editing Netplan YAML files to disable IPv6](#editing-netplan-yaml-files-to-disable-ipv6)
4586
4587-For simple tasks, you can use `netplan set` to change your configuration.
4588
4589-In the example below you are going to create a new YAML file called `second-interface.yaml` containing only the configuration needed to enable our second interfaces.
4590+## Using `netplan set` to enable a network interface with DHCP
4591
4592-Considering your second network interface is `enp6s0`, run the command below:
4593+For simple configuration changes, use the `netplan set` command. In the example below, you are going to create a new YAML file called `second-interface.yaml` containing only the configuration needed to enable the second network interface.
4594
4595-```
4596-netplan set --origin-hint second-interface ethernets.enp6s0.dhcp4=true
4597-```
4598+1. To create a second network interface called `enp6s0`, run:
4599
4600-The command line parameter `--origin-hint` sets the name of the file where the configuration will be stored.
4601+ ```
4602+ netplan set --origin-hint second-interface ethernets.enp6s0.dhcp4=true
4603+ ```
4604
4605-Now list the files in the directory `/etc/netplan`:
4606+ The `--origin-hint` command-line parameter sets the name of the file in which the configuration is stored.
4607
4608-```
4609-ls /etc/netplan
4610-```
4611+2. List the files in the directory `/etc/netplan`:
4612
4613-You should see the auto generated `cloud-init` file and a new file called `second-interface.yaml`:
4614+ ```none
4615+ ls -1 /etc/netplan
4616+ ```
4617
4618-```
4619-root@netplan-lab0:~# ls /etc/netplan/
4620-50-cloud-init.yaml second-interface.yaml
4621-```
4622-
4623-Use the command `cat` to see its content:
4624-
4625-```
4626-cat /etc/netplan/second-interface.yaml
4627-```
4628-
4629-```yaml
4630-network:
4631- version: 2
4632- ethernets:
4633- enp6s0:
4634- dhcp4: true
4635-```
4636-
4637-You will notice it is very similar to the one generated by `cloud-init`.
4638-
4639-Now use `netplan get` to see your full configuration:
4640-
4641-```
4642-netplan get
4643-```
4644+ You should see the auto-generated `cloud-init` file and a new file called `second-interface.yaml`:
4645
4646-You should see an output similar to the one below with both Ethernet interfaces:
4647+ ```none
4648+ 50-cloud-init.yaml
4649+ second-interface.yaml
4650+ ```
4651
4652-```yaml
4653-network:
4654- version: 2
4655- ethernets:
4656- enp5s0:
4657- dhcp4: true
4658- enp6s0:
4659- dhcp4: true
4660-```
4661+3. Use the command `cat` to see the file content:
4662
4663-## Applying your new configuration
4664+ ```
4665+ cat /etc/netplan/second-interface.yaml
4666+ ```
4667
4668-The command `netplan set` created the configuration for your second network interface but it wasn't applied to the running system.
4669+ ```yaml
4670+ network:
4671+ version: 2
4672+ ethernets:
4673+ enp6s0:
4674+ dhcp4: true
4675+ ```
4676
4677-Run the command below to see the current state of your second network interface:
4678+ Notice it is similar to the configuration file generated by `cloud-init` ([Checking Netplan configuration files](#checking-netplan-configuration-files)).
4679
4680-```
4681-ip address show enp6s0
4682-```
4683+4. Check the full configuration using `netplan get`:
4684
4685-You should see an output similar to the one below:
4686+ ```
4687+ netplan get
4688+ ```
4689
4690-```
4691-3: enp6s0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
4692- link/ether 00:16:3e:0c:97:8a brd ff:ff:ff:ff:ff:ff
4693-```
4694+ You should see an output similar to the one below with both Ethernet interfaces:
4695
4696-As you can see, this interface has no IP address and its state is DOWN.
4697+ ```yaml
4698+ network:
4699+ version: 2
4700+ ethernets:
4701+ enp5s0:
4702+ dhcp4: true
4703+ enp6s0:
4704+ dhcp4: true
4705+ ```
4706
4707-In order to apply the Netplan configuration, you can use the command `netplan apply`.
4708+The interface configuration has been created. To apply the changes to the system, follow the instructions in [Applying new Netplan configuration](#applying-new-netplan-configuration).
4709
4710-Run the command below in your shell:
4711
4712-```
4713-netplan apply
4714-```
4715+## Editing Netplan YAML files to disable IPv6
4716
4717-Now check again the state of the interface `enp6s0`:
4718+For more complex settings, you can edit existing or create new configuration files manually.
4719
4720-```
4721-ip address show enp6s0
4722-```
4723+For example, to disable automatic IPv6 configuration on the second network interface created in [Using netplan set to enable a network interface with DHCP](#using-netplan-set-to-enable-a-network-interface-with-dhcp), edit the `/etc/netplan/second-interface.yaml` file:
4724
4725-You should see an output similar to this:
4726+1. Add the following lines to the configuration section of the interface:
4727
4728-```
4729-3: enp6s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
4730- link/ether 00:16:3e:0c:97:8a brd ff:ff:ff:ff:ff:ff
4731- inet 10.33.59.157/24 metric 100 brd 10.33.59.255 scope global dynamic enp6s0
4732- valid_lft 3589sec preferred_lft 3589sec
4733- inet6 fd42:ee65:61d0:abcb:216:3eff:fe0c:978a/64 scope global dynamic mngtmpaddr noprefixroute
4734- valid_lft 3599sec preferred_lft 3599sec
4735- inet6 fe80::216:3eff:fe0c:978a/64 scope link
4736- valid_lft forever preferred_lft forever
4737-```
4738+ ```yaml
4739+ accept-ra: false
4740+ link-local: []
4741+ ```
4742
4743-You can also use `netplan status` to check the interface:
4744+ When you finish, the whole configuration in `second-interface.yaml` should look like this:
4745
4746-```
4747-netplan status enp6s0
4748-```
4749+ ```yaml
4750+ network:
4751+ version: 2
4752+ ethernets:
4753+ enp6s0:
4754+ dhcp4: true
4755+ accept-ra: false
4756+ link-local: []
4757+ ```
4758
4759-You should see an output similar to this:
4760+ With this new configuration, the network configuration back end (`systemd-networkd` in this case) does not accept Route Advertisements and does not add the `link-local` address to the interface.
4761
4762-```
4763- Online state: online
4764- DNS Addresses: 127.0.0.53 (stub)
4765- DNS Search: lxd
4766+2. Check the new configuration using the `netplan get` command:
4767
4768-● 3: enp6s0 ethernet UP (networkd: enp6s0)
4769- MAC Address: 00:16:3e:0c:97:8a (Red Hat, Inc.)
4770- Addresses: 10.33.59.157/24 (dhcp)
4771- fd42:ee65:61d0:abcb:216:3eff:fe0c:978a/64
4772- fe80::216:3eff:fe0c:978a/64 (link)
4773- DNS Addresses: 10.33.59.1
4774- fe80::216:3eff:fea1:585b
4775- DNS Search: lxd
4776- Routes: default via 10.33.59.1 from 10.33.59.157 metric 100 (dhcp)
4777- 10.33.59.0/24 from 10.33.59.157 metric 100 (link)
4778- 10.33.59.1 from 10.33.59.157 metric 100 (dhcp, link)
4779- fd42:ee65:61d0:abcb::/64 metric 100 (ra)
4780- fe80::/64 metric 256
4781+ ```
4782+ netplan get
4783+ ```
4784
4785-2 inactive interfaces hidden. Use "--all" to show all.
4786-```
4787+ You should see something similar to this:
4788
4789-As you can see, even though you haven't enabled DHCP for IPv6 on this interface, the network configuration back end (in this case systemd-networkd) enabled it anyway. But let's assume you want only IPv4.
4790+ ```yaml
4791+ network:
4792+ version: 2
4793+ ethernets:
4794+ enp5s0:
4795+ dhcp4: true
4796+ enp6s0:
4797+ dhcp4: true
4798+ accept-ra: false
4799+ link-local: []
4800+ ```
4801
4802-Let's address this situation in the next exercise.
4803+IPv6 has been disabled for the interface in the configuration. To apply the changes to the system, follow the instructions in [Applying new Netplan configuration](#applying-new-netplan-configuration).
4804
4805-## Editing YAML files
4806
4807-For more complex configuration, you can just create or edit a new file yourself using your favourite text editor.
4808+# Applying new Netplan configuration
4809
4810-Continuing the exercise from the previous section, let's go ahead and disable automatic IPv6 configuration on your second interface. But this time let's do it by manually editing the YAML file.
4811+New or modified Netplan settings need to be applied before they take effect on a running system.
4812
4813-Use your favourite text editor and open the file `/etc/netplan/second-interface.yaml`.
4814+:::{note}
4815+Using the `netplan set` command to modify configuration or editing (creating) the Netplan YAML configuration files directly does not automatically apply the new settings to the running system.
4816+:::
4817
4818-Add the configuration below to the interface configuration section:
4819+After creating a new configuration as described in [Using netplan set to enable a network interface with DHCP](#using-netplan-set-to-enable-a-network-interface-with-dhcp), follow these steps to apply the settings and confirm they have taken effect.
4820
4821-```yaml
4822-accept-ra: false
4823-link-local: []
4824-```
4825+1. Display the current state of the network interface:
4826
4827-When you finish, it should look like this:
4828+ ```
4829+ ip address show enp6s0
4830+ ```
4831
4832-```yaml
4833-network:
4834- version: 2
4835- ethernets:
4836- enp6s0:
4837- dhcp4: true
4838- accept-ra: false
4839- link-local: []
4840-```
4841+ Where `enp6s0` is the interface you wish to display status for. You should see an output similar to the one below:
4842
4843-With this new configuration, the network configuration back end (systemd-networkd in this case) will not accept Route Advertisements and will not add the link-local address to our interface.
4844+ ```none
4845+ 3: enp6s0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
4846+ link/ether 00:16:3e:0c:97:8a brd ff:ff:ff:ff:ff:ff
4847+ ```
4848
4849-Now check your new configuration with the `netplan get` command:
4850+ This interface has no IP address and its state is `DOWN`.
4851
4852-```
4853-netplan get
4854-```
4855+2. Apply the new Netplan configuration:
4856
4857-You should see something similar to this:
4858+ ```
4859+ netplan apply
4860+ ```
4861
4862-```yaml
4863-network:
4864- version: 2
4865- ethernets:
4866- enp5s0:
4867- dhcp4: true
4868- enp6s0:
4869- dhcp4: true
4870- accept-ra: false
4871- link-local: []
4872-```
4873+3. Check the state of the `enp6s0` interface again using one of the following two methods:
4874
4875-Now use `netplan apply` to apply your new configuration:
4876+ * Using the `ip` tool:
4877
4878-```
4879-netplan apply
4880-```
4881+ ```
4882+ ip address show enp6s0
4883+ ```
4884
4885-And check your interface configuration:
4886+ You should see an output similar to this:
4887
4888-```
4889-ip address show enp6s0
4890-```
4891+ ```none
4892+ 3: enp6s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
4893+ link/ether 00:16:3e:0c:97:8a brd ff:ff:ff:ff:ff:ff
4894+ inet 10.33.59.157/24 metric 100 brd 10.33.59.255 scope global dynamic enp6s0
4895+ valid_lft 3589sec preferred_lft 3589sec
4896+ ```
4897
4898-You should see an output similar to this:
4899+ * Using the `netplan status` command:
4900
4901-```
4902-3: enp6s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
4903- link/ether 00:16:3e:0c:97:8a brd ff:ff:ff:ff:ff:ff
4904- inet 10.33.59.157/24 metric 100 brd 10.33.59.255 scope global dynamic enp6s0
4905- valid_lft 3585sec preferred_lft 3585sec
4906-```
4907+ ```
4908+ netplan status enp6s0
4909+ ```
4910
4911-And as you can see, now it only has an IPv4 address.
4912+ You should see an output similar to this:
4913
4914-In this exercise you explored the `netplan set`, `netplan get`, `netplan apply` and `netplan status` commands. You also used some of the Ethernet configuration options to get a network interface up and running with DHCP.
4915+ ```
4916+ Online state: online
4917+ DNS Addresses: 127.0.0.53 (stub)
4918+ DNS Search: lxd
4919
4920-# Using static IP addresses
4921+ ● 3: enp6s0 ethernet UP (networkd: enp6s0)
4922+ MAC Address: 00:16:3e:0c:97:8a (Red Hat, Inc.)
4923+ Addresses: 10.33.59.157/24 (dhcp)
4924+ DNS Addresses: 10.33.59.1
4925+ DNS Search: lxd
4926+ Routes: default via 10.33.59.1 from 10.33.59.157 metric 100 (dhcp)
4927+ 10.33.59.0/24 from 10.33.59.157 metric 100 (link)
4928+ 10.33.59.1 from 10.33.59.157 metric 100 (dhcp, link)
4929
4930-In this exercise you're going to add an static IP address to the second interface with a default route and DNS configuration.
4931+ 2 inactive interfaces hidden. Use "--all" to show all.
4932+ ```
4933
4934-Edit the file `/etc/netplan/second-interface.yaml` created previously. Change it so it will look like this:
4935+---
4936
4937-```yaml
4938-network:
4939- version: 2
4940- ethernets:
4941- enp6s0:
4942- dhcp4: false
4943- dhcp6: false
4944- accept-ra: false
4945- link-local: []
4946- addresses:
4947- - 172.16.0.1/24
4948- routes:
4949- - to: default
4950- via: 172.16.0.254
4951- nameservers:
4952- search:
4953- - netplanlab.local
4954- addresses:
4955- - 172.16.0.254
4956- - 172.16.0.253
4957-```
4958-
4959-The configuration above is what you'd expect in a desktop system for example. It defines the interface's IP address statically as `172.16.0.1/24`, a default route via gateway `172.16.0.254` and the DNS search domain and name servers.
4960-
4961-Now use `netplan get` to visualise all your network configuration:
4962-
4963-```
4964-netplan get
4965-```
4966-
4967-You should see an output similar to this:
4968-
4969-```yaml
4970-network:
4971- version: 2
4972- ethernets:
4973- enp5s0:
4974- dhcp4: true
4975- enp6s0:
4976- addresses:
4977- - "172.16.0.1/24"
4978- nameservers:
4979- addresses:
4980- - 172.16.0.254
4981- - 172.16.0.253
4982- search:
4983- - netplanlab.local
4984- dhcp4: false
4985- dhcp6: false
4986- accept-ra: false
4987- routes:
4988- - to: "default"
4989- via: "172.16.0.254"
4990- link-local: []
4991-```
4992-
4993-You will notice that it might be a little different than what you have defined in the YAML file. Some things might be in a different order for example.
4994-
4995-The reason for that is that `netplan get` loads and parses your configuration before outputting it, and the YAML parsing engine used by Netplan might shuffle things around. Although, what you see from `netplan get` is equivalent to what you have in the file.
4996-
4997-Now use `netplan apply` to apply the new configuration:
4998-
4999-```
5000-netplan apply
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches