summary refs log tree commit diff
path: root/contrib
diff options
context:
space:
mode:
Diffstat (limited to 'contrib')
-rwxr-xr-xcontrib/alpine/REQUIRED_PACKAGES53
-rw-r--r--contrib/art/logo/alternate/nm_logo_red.svg81
-rw-r--r--contrib/art/logo/alternate/nm_logo_red_reverse.svg93
-rw-r--r--contrib/art/logo/alternate/nm_logo_reverse.svg93
-rw-r--r--contrib/art/logo/alternate/nm_logotype_vertical.svg170
-rw-r--r--contrib/art/logo/nm_logo.svg81
-rw-r--r--contrib/art/logo/nm_logotype.svg168
-rw-r--r--contrib/art/logo/stickers/nm_sticker_blue.svg180
-rw-r--r--contrib/art/logo/stickers/nm_sticker_red.svg180
-rw-r--r--contrib/art/logo/stickers/nm_sticker_white.svg180
-rwxr-xr-xcontrib/debian/REQUIRED_PACKAGES106
-rw-r--r--contrib/editors/networkmanager-style.el61
-rwxr-xr-xcontrib/fedora/REQUIRED_PACKAGES108
-rw-r--r--contrib/fedora/rpm/00-server.conf14
-rw-r--r--contrib/fedora/rpm/20-connectivity-fedora.conf10
-rw-r--r--contrib/fedora/rpm/20-connectivity-redhat.conf10
-rw-r--r--contrib/fedora/rpm/22-wifi-mac-addr.conf31
-rw-r--r--contrib/fedora/rpm/70-nm-connectivity.conf15
-rw-r--r--contrib/fedora/rpm/NetworkManager.conf52
-rw-r--r--contrib/fedora/rpm/NetworkManager.spec1307
-rw-r--r--contrib/fedora/rpm/README27
-rwxr-xr-xcontrib/fedora/rpm/build.sh288
-rwxr-xr-xcontrib/fedora/rpm/build_clean.sh303
-rwxr-xr-xcontrib/fedora/rpm/configure-for-system.sh526
-rwxr-xr-xcontrib/fedora/rpm/mockbuild.sh20
-rw-r--r--contrib/fedora/rpm/readme-ifcfg-rh-migrated.txt84
-rw-r--r--contrib/fedora/rpm/readme-ifcfg-rh.txt63
-rwxr-xr-xcontrib/fedora/rpm/release.sh604
-rwxr-xr-xcontrib/fedora/utils/makerepo.sh698
-rwxr-xr-xcontrib/scripts/NM-log85
-rwxr-xr-xcontrib/scripts/anonymize-logs.py193
-rwxr-xr-xcontrib/scripts/btmodem.pl291
-rwxr-xr-xcontrib/scripts/checkpatch-feature-branch.sh61
-rwxr-xr-xcontrib/scripts/checkpatch-git-post-commit-hook23
-rwxr-xr-xcontrib/scripts/checkpatch.pl319
-rwxr-xr-xcontrib/scripts/code-style-git-post-commit-hook21
-rwxr-xr-xcontrib/scripts/find-backports417
-rwxr-xr-xcontrib/scripts/git-backport-merge58
-rwxr-xr-xcontrib/scripts/git-subtree-reimport.sh63
-rwxr-xr-xcontrib/scripts/modemu.pl299
-rwxr-xr-xcontrib/scripts/nm-ci-patch-gtkdoc.sh40
-rwxr-xr-xcontrib/scripts/nm-ci-run.sh299
-rwxr-xr-xcontrib/scripts/nm-code-format-container.sh46
-rwxr-xr-xcontrib/scripts/nm-code-format.sh222
-rwxr-xr-xcontrib/scripts/nm-copr-build-nm-git-bundle.sh95
-rwxr-xr-xcontrib/scripts/nm-copr-build.sh101
-rwxr-xr-xcontrib/scripts/nm-import-openconnect261
-rwxr-xr-xcontrib/scripts/nm-import-openvpn543
-rwxr-xr-xcontrib/scripts/nm-import-vpnc416
-rwxr-xr-xcontrib/scripts/nm-python-black-format.sh100
-rwxr-xr-xcontrib/scripts/nm-setup-git.sh134
-rwxr-xr-xcontrib/scripts/test-create-many-device-setup.sh136
-rwxr-xr-xcontrib/scripts/test-macsec102
-rwxr-xr-xcontrib/scripts/test-ppp.sh108
-rwxr-xr-xcontrib/scripts/test-prefix-delegation.sh151
55 files changed, 10190 insertions, 0 deletions
diff --git a/contrib/alpine/REQUIRED_PACKAGES b/contrib/alpine/REQUIRED_PACKAGES
new file mode 100755
index 00000000..1b299b2e
--- /dev/null
+++ b/contrib/alpine/REQUIRED_PACKAGES
@@ -0,0 +1,53 @@
+#!/bin/sh
+
+set -ex
+
+apk update
+
+apk add \
+    'alpine-sdk' \
+    'autoconf' \
+    'automake' \
+    'bash' \
+    'clang' \
+    'curl-dev' \
+    'dbus' \
+    'dbus-glib-dev' \
+    'elogind-dev' \
+    'eudev-dev' \
+    'gcc' \
+    'git' \
+    'gnutls-dev' \
+    'gobject-introspection-dev' \
+    'gtk-doc' \
+    'intltool' \
+    'iproute2' \
+    'iptables' \
+    'jansson-dev' \
+    'libgudev-dev' \
+    'libndp-dev' \
+    'libnl3-dev' \
+    'libpsl-dev' \
+    'libsoup-dev' \
+    'libteam-dev' \
+    'libtool' \
+    'linux-headers' \
+    'make' \
+    'meson' \
+    'mobile-broadband-provider-info' \
+    'modemmanager-dev' \
+    'musl-dev' \
+    'newt-dev' \
+    'nss-dev' \
+    'polkit-dev' \
+    'ppp' \
+    'ppp-dev' \
+    'py3-dbus' \
+    'py3-gobject3' \
+    'py3-pexpect' \
+    'python3' \
+    'readline-dev' \
+    'util-linux-dev' \
+    'vala' \
+    'valgrind' \
+    'vim'
diff --git a/contrib/art/logo/alternate/nm_logo_red.svg b/contrib/art/logo/alternate/nm_logo_red.svg
new file mode 100644
index 00000000..50c26b70
--- /dev/null
+++ b/contrib/art/logo/alternate/nm_logo_red.svg
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="50mm"
+   height="50mm"
+   viewBox="0 0 49.999999 49.999999"
+   version="1.1"
+   id="svg3790"
+   inkscape:version="0.92.4 (unknown)"
+   sodipodi:docname="nm_logo_red.svg">
+  <defs
+     id="defs3784" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="2.8"
+     inkscape:cx="89.730775"
+     inkscape:cy="89.762394"
+     inkscape:document-units="mm"
+     inkscape:current-layer="logo"
+     showgrid="false"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     inkscape:window-width="1920"
+     inkscape:window-height="1136"
+     inkscape:window-x="1280"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata3787">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="layer01"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-87.115936,-83.441986)">
+    <g
+       id="logo"
+       transform="matrix(0.01396314,0,0,0.01396314,18.403102,71.599589)"
+       style="stroke-width:0.58136749"
+       inkscape:label="logo">
+      <path
+         sodipodi:nodetypes="ccccccccccccccccccccccccc"
+         style="fill:#cc0000;fill-opacity:1;stroke:none;stroke-width:0.77839899"
+         d="M 4921.0159,848.11845 V 4427.6574 h 698.5017 V 2014.3905 l 513.5931,516.0984 -242.4055,243.5876 678.4845,681.8654 242.75,-244.6281 1210.4382,1216.3436 1.3107,1.3175 1.3117,-1.3175 h 476.8721 V 848.12395 H 8023.6883 L 6935.0748,1942.0493 6826.9864,1833.2256 6612.6048,2048.6539 5429.8454,848.11845 Z M 7824.6995,2011.8244 v 1253.4504 l -533.539,-536.1426 213.0014,-214.0412 -90.0745,-90.7209 z"
+         id="path1482"
+         inkscape:connector-curvature="0"
+         inkscape:label="cable" />
+      <path
+         sodipodi:nodetypes="ccccccccccccccccccccc"
+         style="fill:#ffffff;stroke:none;stroke-width:0.77839899"
+         d="m 6828.2981,2003.7092 -765.3151,769.0494 88.9985,91.2718 203.6838,-204.6779 34.7174,36.2748 36.0988,34.8874 -203.683,204.6775 63.4313,63.7406 202.9925,-203.9839 34.7884,36.2051 36.0279,34.8878 -193.8409,211.4097 57.2489,57.5297 203.0644,-203.984 34.7166,36.2053 36.0304,34.9569 -202.9934,203.984 77.6182,77.997 765.3149,-769.0494 -255.1047,-255.0319 z"
+         id="path1498"
+         inkscape:connector-curvature="0"
+         inkscape:label="connector" />
+    </g>
+  </g>
+</svg>
diff --git a/contrib/art/logo/alternate/nm_logo_red_reverse.svg b/contrib/art/logo/alternate/nm_logo_red_reverse.svg
new file mode 100644
index 00000000..a3e95044
--- /dev/null
+++ b/contrib/art/logo/alternate/nm_logo_red_reverse.svg
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="50mm"
+   height="50mm"
+   viewBox="0 0 50 50"
+   version="1.1"
+   id="svg18"
+   inkscape:version="0.92.4 (unknown)"
+   sodipodi:docname="nm_logo_red_reverse.svg"
+   inkscape:export-xdpi="96"
+   inkscape:export-ydpi="96">
+  <defs
+     id="defs12" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1.979899"
+     inkscape:cx="199.00923"
+     inkscape:cy="55.594808"
+     inkscape:document-units="mm"
+     inkscape:current-layer="logo"
+     showgrid="false"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     inkscape:window-width="1920"
+     inkscape:window-height="1136"
+     inkscape:window-x="1280"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata15">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-53.113205,-106.86279)">
+    <g
+       id="g13">
+      <rect
+         y="106.86279"
+         x="53.113205"
+         height="50"
+         width="50"
+         id="rect22"
+         style="fill:#cc0000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.37634408;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <g
+         id="logo"
+         transform="matrix(0.01061199,0,0,0.01061199,6.8914335,103.86255)"
+         style="stroke-width:0.58136749"
+         inkscape:label="logo">
+        <path
+           sodipodi:nodetypes="ccccccccccccccccccccccccc"
+           style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.77839899"
+           d="M 4921.0159,848.11845 V 4427.6574 h 698.5017 V 2014.3905 l 513.5931,516.0984 -242.4055,243.5876 678.4845,681.8654 242.75,-244.6281 1210.4382,1216.3436 1.3107,1.3175 1.3117,-1.3175 h 476.8721 V 848.12395 H 8023.6883 L 6935.0748,1942.0493 6826.9864,1833.2256 6612.6048,2048.6539 5429.8454,848.11845 Z M 7824.6995,2011.8244 v 1253.4504 l -533.539,-536.1426 213.0014,-214.0412 -90.0745,-90.7209 z"
+           id="path1482"
+           inkscape:connector-curvature="0"
+           inkscape:label="cable" />
+        <path
+           sodipodi:nodetypes="ccccccccccccccccccccc"
+           style="fill:#cc0000;fill-opacity:1;stroke:none;stroke-width:0.77839899"
+           d="m 6828.2981,2003.7092 -765.3151,769.0494 88.9985,91.2718 203.6838,-204.6779 34.7174,36.2748 36.0988,34.8874 -203.683,204.6775 63.4313,63.7406 202.9925,-203.9839 34.7884,36.2051 36.0279,34.8878 -193.8409,211.4097 57.2489,57.5297 203.0644,-203.984 34.7166,36.2053 36.0304,34.9569 -202.9934,203.984 77.6182,77.997 765.3149,-769.0494 -255.1047,-255.0319 z"
+           id="path1498"
+           inkscape:connector-curvature="0"
+           inkscape:label="connector" />
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/contrib/art/logo/alternate/nm_logo_reverse.svg b/contrib/art/logo/alternate/nm_logo_reverse.svg
new file mode 100644
index 00000000..0f863ea2
--- /dev/null
+++ b/contrib/art/logo/alternate/nm_logo_reverse.svg
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="50mm"
+   height="50mm"
+   viewBox="0 0 50 50"
+   version="1.1"
+   id="svg18"
+   inkscape:version="0.92.4 (unknown)"
+   sodipodi:docname="nm_logo_reverse.svg"
+   inkscape:export-xdpi="96"
+   inkscape:export-ydpi="96">
+  <defs
+     id="defs12" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1.979899"
+     inkscape:cx="199.00923"
+     inkscape:cy="55.594808"
+     inkscape:document-units="mm"
+     inkscape:current-layer="g13"
+     showgrid="false"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     inkscape:window-width="1920"
+     inkscape:window-height="1136"
+     inkscape:window-x="1280"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata15">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-53.113205,-106.86279)">
+    <g
+       id="g13">
+      <rect
+         y="106.86279"
+         x="53.113205"
+         height="50"
+         width="50"
+         id="rect22"
+         style="fill:#32557d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.37634408;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <g
+         id="logo"
+         transform="matrix(0.01061199,0,0,0.01061199,6.8914335,103.86255)"
+         style="stroke-width:0.58136749"
+         inkscape:label="logo">
+        <path
+           sodipodi:nodetypes="ccccccccccccccccccccccccc"
+           style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.77839899"
+           d="M 4921.0159,848.11845 V 4427.6574 h 698.5017 V 2014.3905 l 513.5931,516.0984 -242.4055,243.5876 678.4845,681.8654 242.75,-244.6281 1210.4382,1216.3436 1.3107,1.3175 1.3117,-1.3175 h 476.8721 V 848.12395 H 8023.6883 L 6935.0748,1942.0493 6826.9864,1833.2256 6612.6048,2048.6539 5429.8454,848.11845 Z M 7824.6995,2011.8244 v 1253.4504 l -533.539,-536.1426 213.0014,-214.0412 -90.0745,-90.7209 z"
+           id="path1482"
+           inkscape:connector-curvature="0"
+           inkscape:label="cable" />
+        <path
+           sodipodi:nodetypes="ccccccccccccccccccccc"
+           style="fill:#32557d;fill-opacity:1;stroke:none;stroke-width:0.77839899"
+           d="m 6828.2981,2003.7092 -765.3151,769.0494 88.9985,91.2718 203.6838,-204.6779 34.7174,36.2748 36.0988,34.8874 -203.683,204.6775 63.4313,63.7406 202.9925,-203.9839 34.7884,36.2051 36.0279,34.8878 -193.8409,211.4097 57.2489,57.5297 203.0644,-203.984 34.7166,36.2053 36.0304,34.9569 -202.9934,203.984 77.6182,77.997 765.3149,-769.0494 -255.1047,-255.0319 z"
+           id="path1498"
+           inkscape:connector-curvature="0"
+           inkscape:label="connector" />
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/contrib/art/logo/alternate/nm_logotype_vertical.svg b/contrib/art/logo/alternate/nm_logotype_vertical.svg
new file mode 100644
index 00000000..8f794128
--- /dev/null
+++ b/contrib/art/logo/alternate/nm_logotype_vertical.svg
@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="75mm"
+   height="85mm"
+   viewBox="0 0 75 85"
+   version="1.1"
+   id="svg71"
+   inkscape:version="0.92.4 (unknown)"
+   sodipodi:docname="nm_logotype_vertical.svg">
+  <defs
+     id="defs65" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1"
+     inkscape:cx="142.45306"
+     inkscape:cy="192.65794"
+     inkscape:document-units="mm"
+     inkscape:current-layer="g1627"
+     showgrid="false"
+     fit-margin-top="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     fit-margin-left="0"
+     inkscape:window-width="1920"
+     inkscape:window-height="1136"
+     inkscape:window-x="1280"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata68">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-46.225296,-129.40381)">
+    <g
+       id="g1627"
+       transform="matrix(0.30239406,0,0,0.30239406,-107.25959,-16.360685)"
+       style="stroke-width:0.87496203">
+      <g
+         id="g1538"
+         transform="matrix(0.03779528,0,0,0.03779528,358.79361,490.17043)"
+         style="stroke-width:0.87496203"
+         inkscape:label="logo">
+        <path
+           inkscape:connector-curvature="0"
+           id="path1534"
+           d="M 5029.9666,-215.25215 V 4157.9482 h 853.3745 V 1209.6083 l 627.4666,630.5285 -296.1503,297.5952 828.9187,833.0503 296.5727,-298.8683 1478.8184,1486.0342 1.6018,1.6095 1.6016,-1.6095 h 582.6059 V -215.24452 H 8820.5686 L 7490.5865,1121.2273 7358.5311,988.27476 7096.6167,1251.4682 5651.6129,-215.25215 Z M 8577.4587,1206.4725 v 1531.3676 l -651.8359,-655.0173 260.2277,-261.4989 -110.0444,-110.8354 z"
+           style="fill:#32557d;fill-opacity:1;stroke:none;stroke-width:1.43124211"
+           sodipodi:nodetypes="ccccccccccccccccccccccccc" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path1536"
+           d="m 7360.1336,1196.5577 -935.0009,939.5654 108.7309,111.5086 248.8436,-250.0604 42.4166,44.3192 44.1019,42.6221 -248.8434,250.0585 77.4949,77.8737 248.0001,-249.2123 42.5016,44.2339 44.0169,42.6214 -236.8198,258.2846 69.9416,70.285 248.0871,-249.2115 42.4169,44.2339 44.0168,42.7063 -248.0007,249.2114 94.8286,95.2918 935,-939.5654 -311.6662,-311.577 z"
+           style="fill:#ffffff;stroke:none;stroke-width:1.43124211"
+           sodipodi:nodetypes="ccccccccccccccccccccc" />
+      </g>
+      <g
+         style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.02106726;image-rendering:auto"
+         transform="matrix(1.3214654,0,0,1.3214654,323.73031,623.26439)"
+         id="g1548"
+         inkscape:label="g1548">
+        <g
+           aria-label="Network"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.33208466px;line-height:1.25;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62478876px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.02106726"
+           id="text1542">
+          <path
+             d="M 160.11453,33.334168 V 51.849521 L 144.95372,33.334168 h -5.83778 v 30.495876 h 6.97049 V 45.314691 l 15.20437,18.515353 h 5.79421 V 33.334168 Z"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:43.5655365px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62478876px;writing-mode:lr-tb;text-anchor:start"
+             id="path16"
+             inkscape:connector-curvature="0" />
+          <path
+             d="m 177.2528,58.558614 v -4.922906 h 11.41417 V 48.625672 H 177.2528 V 44.05129 h 12.8954 v -5.27143 h -19.69162 v 25.050184 h 20.17084 v -5.27143 z"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:43.5655365px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62478876px;writing-mode:lr-tb;text-anchor:start"
+             id="path18"
+             inkscape:connector-curvature="0" />
+          <path
+             d="m 196.72319,63.830044 h 6.88336 V 44.138421 h 7.88536 V 38.77986 h -22.65408 v 5.358561 h 7.88536 z"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:43.5655365px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62478876px;writing-mode:lr-tb;text-anchor:start"
+             id="path20"
+             inkscape:connector-curvature="0" />
+          <path
+             d="m 243.35874,38.77986 -5.48925,16.685601 -5.315,-16.685601 h -6.36057 l -5.48926,16.380642 -5.27143,-16.380642 h -7.10118 l 8.32102,25.050184 h 7.36257 l 5.22787,-15.857855 5.09717,15.857855 h 7.36257 L 249.9807,38.77986 Z"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:43.5655365px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62478876px;writing-mode:lr-tb;text-anchor:start"
+             id="path22"
+             inkscape:connector-curvature="0" />
+          <path
+             d="m 261.84822,64.309265 c 8.10319,0 14.07167,-5.489258 14.07167,-12.98253 0,-7.536838 -5.96848,-13.026096 -14.07167,-13.026096 -8.10319,0 -14.07166,5.489258 -14.07166,13.026096 0,7.493272 5.96847,12.98253 14.07166,12.98253 z m 0,-5.619955 c -4.09516,0 -7.14474,-3.093153 -7.14474,-7.362575 0,-4.312988 3.04958,-7.406141 7.14474,-7.406141 4.09516,0 7.10119,3.093153 7.10119,7.406141 0,4.269422 -3.00603,7.362575 -7.10119,7.362575 z"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:43.5655365px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62478876px;writing-mode:lr-tb;text-anchor:start"
+             id="path24"
+             inkscape:connector-curvature="0" />
+          <path
+             d="m 292.97988,63.830044 h 7.40615 l -5.57639,-7.885362 c 3.22385,-1.481229 5.09717,-4.225857 5.09717,-7.928928 0,-5.794216 -4.40012,-9.235894 -11.28348,-9.235894 h -11.41417 v 25.050184 h 6.88336 v -6.709093 h 4.22585 z m 0,-15.81429 c 0,2.43967 -1.61192,3.877333 -4.83577,3.877333 h -4.05159 v -7.754666 h 4.05159 c 3.22385,0 4.83577,1.394097 4.83577,3.877333 z"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:43.5655365px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62478876px;writing-mode:lr-tb;text-anchor:start"
+             id="path26"
+             inkscape:connector-curvature="0" />
+          <path
+             d="m 318.78498,63.830044 h 8.01605 L 315.82252,49.932638 326.23468,38.77986 h -7.5804 L 308.45994,49.453417 V 38.77986 h -6.79622 v 25.050184 h 6.79622 v -6.317003 l 2.83176,-2.918891 z"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:43.5655365px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62478876px;writing-mode:lr-tb;text-anchor:start"
+             id="path28"
+             inkscape:connector-curvature="0" />
+        </g>
+        <g
+           aria-label="Manager"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.33208466px;line-height:1.25;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.02106726"
+           id="text1546">
+          <path
+             d="m 172.951,105.36586 -0.0858,-30.019376 h -5.70368 L 156.09727,94.001382 144.86144,75.346484 h -5.74656 v 30.019376 h 6.51849 V 87.740198 l 8.79139,14.452182 h 3.13059 l 8.83427,-14.838146 0.0429,18.011626 z"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.88482285px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.45056129px;writing-mode:lr-tb;text-anchor:start"
+             id="path31"
+             inkscape:connector-curvature="0" />
+          <path
+             d="m 195.84872,105.36586 h 6.69003 L 191.43158,80.707087 h -6.69003 l -11.06428,24.658773 h 6.56137 l 2.05847,-4.88887 h 11.53602 z m -11.49313,-9.77774 3.73098,-8.920043 3.68809,8.920043 z"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.88482285px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.45056129px;writing-mode:lr-tb;text-anchor:start"
+             id="path33"
+             inkscape:connector-curvature="0" />
+          <path
+             d="M 219.91371,80.707087 V 94.344461 L 208.54923,80.707087 h -5.57503 v 24.658773 h 6.64715 V 91.771371 l 11.36448,13.594489 h 5.57503 V 80.707087 Z"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.88482285px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.45056129px;writing-mode:lr-tb;text-anchor:start"
+             id="path35"
+             inkscape:connector-curvature="0" />
+          <path
+             d="m 249.16039,105.36586 h 6.69003 L 244.74325,80.707087 h -6.69003 l -11.06428,24.658773 h 6.56138 l 2.05847,-4.88887 h 11.53601 z m -11.49313,-9.77774 3.73098,-8.920043 3.68809,8.920043 z"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.88482285px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.45056129px;writing-mode:lr-tb;text-anchor:start"
+             id="path37"
+             inkscape:connector-curvature="0" />
+          <path
+             d="m 272.07754,99.533524 c -1.20077,0.514616 -2.44443,0.771926 -3.85963,0.771926 -4.37425,0 -7.33331,-2.959052 -7.33331,-7.247534 0,-4.374252 3.04483,-7.29042 7.24754,-7.29042 2.44443,0 4.54579,0.900581 6.2183,2.873283 l 4.46002,-3.988288 c -2.44443,-2.959053 -5.96099,-4.417137 -10.76409,-4.417137 -8.10523,0 -13.98045,5.403487 -13.98045,12.822562 0,7.376194 5.74656,12.779674 14.02333,12.779674 3.47368,0 7.41908,-1.115 10.16371,-3.13059 v -9.992163 h -6.17542 z"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.88482285px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.45056129px;writing-mode:lr-tb;text-anchor:start"
+             id="path39"
+             inkscape:connector-curvature="0" />
+          <path
+             d="M 288.33347,100.1768 V 95.330811 H 299.5693 V 90.399057 H 288.33347 V 85.89615 h 12.69391 v -5.189063 h -19.38394 v 24.658773 h 19.85567 v -5.18906 z"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.88482285px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.45056129px;writing-mode:lr-tb;text-anchor:start"
+             id="path41"
+             inkscape:connector-curvature="0" />
+          <path
+             d="m 319.50997,105.36586 h 7.29042 l -5.48926,-7.762153 c 3.17348,-1.458084 5.01753,-4.159828 5.01753,-7.805038 0,-5.703681 -4.33137,-9.091582 -11.10717,-9.091582 h -11.23583 v 24.658773 h 6.77581 v -6.604263 h 4.15982 z m 0,-15.567191 c 0,2.40155 -1.58674,3.81675 -4.76022,3.81675 h -3.98828 V 85.98192 h 3.98828 c 3.17348,0 4.76022,1.372314 4.76022,3.816749 z"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.88482285px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.45056129px;writing-mode:lr-tb;text-anchor:start"
+             id="path43"
+             inkscape:connector-curvature="0" />
+        </g>
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/contrib/art/logo/nm_logo.svg b/contrib/art/logo/nm_logo.svg
new file mode 100644
index 00000000..ce5797dd
--- /dev/null
+++ b/contrib/art/logo/nm_logo.svg
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="50mm"
+   height="50mm"
+   viewBox="0 0 49.999999 49.999999"
+   version="1.1"
+   id="svg3790"
+   inkscape:version="0.92.4 (unknown)"
+   sodipodi:docname="nm_logo.svg">
+  <defs
+     id="defs3784" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="2.8"
+     inkscape:cx="89.730775"
+     inkscape:cy="89.762394"
+     inkscape:document-units="mm"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     inkscape:window-width="1920"
+     inkscape:window-height="1136"
+     inkscape:window-x="1280"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata3787">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="layer01"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-87.115936,-83.441986)">
+    <g
+       id="logo"
+       transform="matrix(0.01396314,0,0,0.01396314,18.403102,71.599589)"
+       style="stroke-width:0.58136749"
+       inkscape:label="logo">
+      <path
+         sodipodi:nodetypes="ccccccccccccccccccccccccc"
+         style="fill:#32557d;fill-opacity:1;stroke:none;stroke-width:0.77839899"
+         d="M 4921.0159,848.11845 V 4427.6574 h 698.5017 V 2014.3905 l 513.5931,516.0984 -242.4055,243.5876 678.4845,681.8654 242.75,-244.6281 1210.4382,1216.3436 1.3107,1.3175 1.3117,-1.3175 h 476.8721 V 848.12395 H 8023.6883 L 6935.0748,1942.0493 6826.9864,1833.2256 6612.6048,2048.6539 5429.8454,848.11845 Z M 7824.6995,2011.8244 v 1253.4504 l -533.539,-536.1426 213.0014,-214.0412 -90.0745,-90.7209 z"
+         id="path1482"
+         inkscape:connector-curvature="0"
+         inkscape:label="cable" />
+      <path
+         sodipodi:nodetypes="ccccccccccccccccccccc"
+         style="fill:#ffffff;stroke:none;stroke-width:0.77839899"
+         d="m 6828.2981,2003.7092 -765.3151,769.0494 88.9985,91.2718 203.6838,-204.6779 34.7174,36.2748 36.0988,34.8874 -203.683,204.6775 63.4313,63.7406 202.9925,-203.9839 34.7884,36.2051 36.0279,34.8878 -193.8409,211.4097 57.2489,57.5297 203.0644,-203.984 34.7166,36.2053 36.0304,34.9569 -202.9934,203.984 77.6182,77.997 765.3149,-769.0494 -255.1047,-255.0319 z"
+         id="path1498"
+         inkscape:connector-curvature="0"
+         inkscape:label="connector" />
+    </g>
+  </g>
+</svg>
diff --git a/contrib/art/logo/nm_logotype.svg b/contrib/art/logo/nm_logotype.svg
new file mode 100644
index 00000000..d9b57db8
--- /dev/null
+++ b/contrib/art/logo/nm_logotype.svg
@@ -0,0 +1,168 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="160mm"
+   height="50mm"
+   viewBox="0 0 160.00002 50"
+   version="1.1"
+   id="svg139"
+   inkscape:version="0.92.4 (unknown)"
+   sodipodi:docname="nm_logotype.svg">
+  <defs
+     id="defs133" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.98994949"
+     inkscape:cx="304.81511"
+     inkscape:cy="-94.216708"
+     inkscape:document-units="mm"
+     inkscape:current-layer="text1510"
+     showgrid="true"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     inkscape:window-width="1920"
+     inkscape:window-height="1136"
+     inkscape:window-x="1280"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1">
+    <inkscape:grid
+       type="xygrid"
+       id="grid48"
+       originx="-14.623603"
+       originy="-3.9577414" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata136">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-18.827967,-33.154443)">
+    <g
+       id="g898"
+       transform="matrix(1.3356542,0,0,1.3421721,-6.3197222,-28.453313)">
+      <path
+         inkscape:connector-curvature="0"
+         id="path1482"
+         d="m 18.827994,45.901532 v 37.239326 h 7.302244 V 58.034709 l 5.369169,5.369172 -2.53414,2.534137 7.09299,7.09371 2.537746,-2.54496 12.654094,12.65409 0.01372,0.01372 0.01372,-0.01372 h 4.985296 V 45.901588 H 51.263822 L 39.883291,57.282115 38.753319,56.149981 36.512137,58.391167 24.147391,45.901532 Z m 30.35554,12.106481 v 13.04013 l -5.577702,-5.577699 2.226749,-2.226753 -0.941643,-0.943805 z"
+         style="fill:#32557d;fill-opacity:1;stroke:none;stroke-width:0.00811772"
+         sodipodi:nodetypes="ccccccccccccccccccccccccc" />
+      <path
+         inkscape:connector-curvature="0"
+         id="path1498"
+         d="m 38.767002,57.923589 -8.000708,8.000719 0.9304,0.949536 2.129339,-2.129343 0.362945,0.377378 0.377382,0.362949 -2.129339,2.129339 0.663121,0.663118 2.122121,-2.122124 0.363668,0.376654 0.376659,0.362953 -2.026453,2.199375 0.59849,0.598505 2.122859,-2.122124 0.362945,0.376658 0.376655,0.363669 -2.122117,2.122124 0.811435,0.811435 8.000711,-8.000719 -2.666904,-2.653194 z"
+         style="fill:#ffffff;stroke:none;stroke-width:0.00811772"
+         sodipodi:nodetypes="ccccccccccccccccccccc" />
+    </g>
+    <g
+       style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.67844689;image-rendering:auto"
+       transform="matrix(0.52342598,0,0,0.52342598,7.5660564,14.086132)"
+       id="g1512">
+      <g
+         aria-label="Network"
+         style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.33237076px;line-height:1.25;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.67844689"
+         id="text1506">
+        <path
+           d="M 159.6069,48.258049 V 66.391555 L 144.75875,48.258049 h -5.71738 V 78.125 h 6.82673 V 59.991494 L 160.75891,78.125 h 5.67472 V 48.258049 Z"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.6670723px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62945271;writing-mode:lr-tb;text-anchor:start;fill:#000000;stroke-width:0.67844689"
+           id="path19"
+           inkscape:connector-curvature="0" />
+        <path
+           d="m 176.3123,72.962284 v -4.821379 h 11.17878 V 63.234192 H 176.3123 v -4.480043 h 12.62946 V 53.591433 H 169.65624 V 78.125 h 19.75486 v -5.162716 z"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.6670723px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62945271;writing-mode:lr-tb;text-anchor:start;fill:#000000;stroke-width:0.67844689"
+           id="path21"
+           inkscape:connector-curvature="0" />
+        <path
+           d="m 195.30173,78.125 h 6.7414 V 58.839483 h 7.72274 v -5.24805 h -22.18688 v 5.24805 h 7.72274 z"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.6670723px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62945271;writing-mode:lr-tb;text-anchor:start;fill:#000000;stroke-width:0.67844689"
+           id="path23"
+           inkscape:connector-curvature="0" />
+        <path
+           d="m 240.89608,53.591433 -5.37605,16.341489 -5.20538,-16.341489 h -6.22939 l -5.37606,16.04282 -5.16271,-16.04282 h -6.95473 L 214.74117,78.125 h 7.21073 L 227.07195,62.594186 232.064,78.125 h 7.21073 l 8.10675,-24.533567 z"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.6670723px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62945271;writing-mode:lr-tb;text-anchor:start;fill:#000000;stroke-width:0.67844689"
+           id="path25"
+           inkscape:connector-curvature="0" />
+        <path
+           d="m 258.92484,78.594338 c 7.93607,0 13.78146,-5.376051 13.78146,-12.714788 0,-7.381403 -5.84539,-12.757454 -13.78146,-12.757454 -7.93608,0 -13.78147,5.376051 -13.78147,12.757454 0,7.338737 5.84539,12.714788 13.78147,12.714788 z m 0,-5.504053 c -4.01071,0 -6.9974,-3.029362 -6.9974,-7.210735 0,-4.22404 2.98669,-7.253402 6.9974,-7.253402 4.0107,0 6.95473,3.029362 6.95473,7.253402 0,4.181373 -2.94403,7.210735 -6.95473,7.210735 z"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.6670723px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62945271;writing-mode:lr-tb;text-anchor:start;fill:#000000;stroke-width:0.67844689"
+           id="path27"
+           inkscape:connector-curvature="0" />
+        <path
+           d="m 289.33503,78.125 h 7.2534 l -5.46138,-7.72274 c 3.15736,-1.450681 4.99205,-4.138706 4.99205,-7.765407 0,-5.674721 -4.30938,-9.04542 -11.05078,-9.04542 H 273.88955 V 78.125 h 6.7414 v -6.570729 h 4.13871 z m 0,-15.488147 c 0,2.389356 -1.57868,3.797369 -4.73604,3.797369 h -3.96804 v -7.594739 h 3.96804 c 3.15736,0 4.73604,1.365347 4.73604,3.79737 z"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.6670723px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62945271;writing-mode:lr-tb;text-anchor:start;fill:#000000;stroke-width:0.67844689"
+           id="path29"
+           inkscape:connector-curvature="0" />
+        <path
+           d="m 314.52853,78.125 h 7.85074 L 311.62717,64.514204 321.8246,53.591433 h -7.42407 l -9.9841,10.453433 V 53.591433 h -6.65606 V 78.125 h 6.65606 v -6.186725 l 2.77336,-2.858694 z"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.6670723px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62945271;writing-mode:lr-tb;text-anchor:start;fill:#000000;stroke-width:0.67844689"
+           id="path31"
+           inkscape:connector-curvature="0" />
+      </g>
+      <g
+         aria-label="Manager"
+         style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.33237076px;line-height:1.25;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.67844689"
+         id="text1510">
+        <path
+           d="m 172.46926,119.66083 -0.0847,-29.632662 h -5.63021 L 155.83264,108.44275 144.74156,90.028168 h -5.67254 v 29.632662 h 6.43452 v -17.39861 l 8.67814,14.26601 h 3.09026 l 8.72047,-14.647 0.0423,17.7796 z"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.33237076px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.76127887px;writing-mode:lr-tb;text-anchor:start;fill:#000000;stroke-width:0.67844689"
+           id="path34"
+           inkscape:connector-curvature="0" />
+        <path
+           d="m 194.72972,119.66083 h 6.60385 L 190.36949,95.319714 h -6.60385 l -10.92175,24.341116 h 6.47685 l 2.03195,-4.82589 h 11.38741 z m -11.34507,-9.65178 3.68291,-8.80514 3.64059,8.80514 z"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.33237076px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.76127887px;writing-mode:lr-tb;text-anchor:start;fill:#000000;stroke-width:0.67844689"
+           id="path36"
+           inkscape:connector-curvature="0" />
+        <path
+           d="M 218.14242,95.319714 V 108.78141 L 206.92434,95.319714 h -5.50321 v 24.341116 h 6.56152 v -13.41936 l 11.21808,13.41936 h 5.5032 V 95.319714 Z"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.33237076px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.76127887px;writing-mode:lr-tb;text-anchor:start;fill:#000000;stroke-width:0.67844689"
+           id="path38"
+           inkscape:connector-curvature="0" />
+        <path
+           d="m 246.67005,119.66083 h 6.60385 L 242.30982,95.319714 h -6.60385 l -10.92175,24.341116 h 6.47685 l 2.03195,-4.82589 h 11.38741 z m -11.34507,-9.65178 3.68291,-8.80514 3.64059,8.80514 z"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.33237076px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.76127887px;writing-mode:lr-tb;text-anchor:start;fill:#000000;stroke-width:0.67844689"
+           id="path40"
+           inkscape:connector-curvature="0" />
+        <path
+           d="m 268.94969,113.90363 c -1.18531,0.50798 -2.41295,0.76198 -3.80991,0.76198 -4.31791,0 -7.23884,-2.92094 -7.23884,-7.15417 0,-4.3179 3.0056,-7.19651 7.15417,-7.19651 2.41295,0 4.48723,0.88898 6.1382,2.83627 l 4.40256,-3.936907 c -2.41294,-2.920934 -5.8842,-4.360235 -10.62542,-4.360235 -8.00082,0 -13.80036,5.333882 -13.80036,12.657382 0,7.28116 5.67254,12.61504 13.84269,12.61504 3.42892,0 7.3235,-1.10064 10.03277,-3.09026 v -9.86344 h -6.09586 z"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.33237076px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.76127887px;writing-mode:lr-tb;text-anchor:start;fill:#000000;stroke-width:0.67844689"
+           id="path42"
+           inkscape:connector-curvature="0" />
+        <path
+           d="m 284.65392,114.53861 v -4.78356 h 11.09109 v -4.86822 h -11.09109 v -4.4449 h 12.53039 v -5.122216 h -19.13424 v 24.341116 h 19.59989 v -5.12222 z"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.33237076px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.76127887px;writing-mode:lr-tb;text-anchor:start;fill:#000000;stroke-width:0.67844689"
+           id="path44"
+           inkscape:connector-curvature="0" />
+        <path
+           d="m 315.08652,119.66083 h 7.1965 l -5.41854,-7.66216 c 3.1326,-1.4393 4.95289,-4.10624 4.95289,-7.70449 0,-5.630208 -4.27557,-8.974466 -10.96409,-8.974466 H 299.7622 v 24.341116 h 6.68852 v -6.51919 h 4.10624 z m 0,-15.36665 c 0,2.37061 -1.5663,3.76758 -4.69889,3.76758 h -3.93691 v -7.53516 h 3.93691 c 3.13259,0 4.69889,1.35463 4.69889,3.76758 z"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.33237076px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.76127887px;writing-mode:lr-tb;text-anchor:start;fill:#000000;stroke-width:0.67844689"
+           id="path46"
+           inkscape:connector-curvature="0" />
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/contrib/art/logo/stickers/nm_sticker_blue.svg b/contrib/art/logo/stickers/nm_sticker_blue.svg
new file mode 100644
index 00000000..a46e22ea
--- /dev/null
+++ b/contrib/art/logo/stickers/nm_sticker_blue.svg
@@ -0,0 +1,180 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="47.889584mm"
+   height="54.9034mm"
+   viewBox="0 0 47.889584 54.9034"
+   version="1.1"
+   id="svg875"
+   inkscape:version="0.92.4 (unknown)"
+   sodipodi:docname="nm_sticker02.svg">
+  <defs
+     id="defs869" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="2.8"
+     inkscape:cx="51.83594"
+     inkscape:cy="64.38986"
+     inkscape:document-units="mm"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     inkscape:window-width="1920"
+     inkscape:window-height="1136"
+     inkscape:window-x="1280"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata872">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-81.055209,-121.0483)">
+    <polygon
+       transform="matrix(0.26458333,0,0,0.26458333,81.055209,120.85104)"
+       id="Polygon-1"
+       points="181,156.37723 90.5,208.25446 -1.5258905e-12,156.37723 -1.5827339e-12,52.622771 90.5,0.74554102 181,52.622771 "
+       style="fill:#24446a;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1" />
+    <polygon
+       transform="matrix(0.26458333,0,0,0.26458333,81.055209,120.85104)"
+       id="Polygon-1-Copy"
+       points="169.18938,59.435072 169.18938,149.56493 90.5,194.62986 11.810617,149.56493 11.810617,59.435072 90.5,14.370145 "
+       style="fill:#32557d;fill-rule:evenodd;stroke:none;stroke-width:1;fill-opacity:1"
+       inkscape:label="Polygon-2" />
+    <g
+       id="g981"
+       transform="matrix(1.0214286,0,0,1.0214286,-2.3225275,-2.3090111)">
+      <g
+         inkscape:label="logo"
+         style="stroke-width:0.87496203"
+         transform="matrix(0.00426687,0,0,0.00426687,74.275548,131.38514)"
+         id="g1538">
+        <path
+           sodipodi:nodetypes="ccccccccccccccccccccccccc"
+           style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.43124211"
+           d="M 5029.9666,-215.25215 V 4157.9482 h 853.3745 V 1209.6083 l 627.4666,630.5285 -296.1503,297.5952 828.9187,833.0503 296.5727,-298.8683 1478.8184,1486.0342 1.6018,1.6095 1.6016,-1.6095 h 582.6059 V -215.24452 H 8820.5686 L 7490.5865,1121.2273 7358.5311,988.27476 7096.6167,1251.4682 5651.6129,-215.25215 Z M 8577.4587,1206.4725 v 1531.3676 l -651.8359,-655.0173 260.2277,-261.4989 -110.0444,-110.8354 z"
+           id="path1534"
+           inkscape:connector-curvature="0" />
+        <path
+           sodipodi:nodetypes="ccccccccccccccccccccc"
+           style="fill:#32557d;fill-opacity:1;stroke:none;stroke-width:1.43124211"
+           d="m 7360.1336,1196.5577 -935.0009,939.5654 108.7309,111.5086 248.8436,-250.0604 42.4166,44.3192 44.1019,42.6221 -248.8434,250.0585 77.4949,77.8737 248.0001,-249.2123 42.5016,44.2339 44.0169,42.6214 -236.8198,258.2846 69.9416,70.285 248.0871,-249.2115 42.4169,44.2339 44.0168,42.7063 -248.0007,249.2114 94.8286,95.2918 935,-939.5654 -311.6662,-311.577 z"
+           id="path1536"
+           inkscape:connector-curvature="0" />
+      </g>
+      <g
+         inkscape:label="g1548"
+         id="g1548"
+         transform="matrix(0.14918522,0,0,0.14918522,70.31712,146.41059)"
+         style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.02106726;image-rendering:auto">
+        <g
+           id="text1542"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.33208466px;line-height:1.25;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62478876px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.02106726"
+           aria-label="Network">
+          <path
+             inkscape:connector-curvature="0"
+             id="path16"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:43.5655365px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62478876px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1"
+             d="M 160.11453,33.334168 V 51.849521 L 144.95372,33.334168 h -5.83778 v 30.495876 h 6.97049 V 45.314691 l 15.20437,18.515353 h 5.79421 V 33.334168 Z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path18"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:43.5655365px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62478876px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1"
+             d="m 177.2528,58.558614 v -4.922906 h 11.41417 V 48.625672 H 177.2528 V 44.05129 h 12.8954 v -5.27143 h -19.69162 v 25.050184 h 20.17084 v -5.27143 z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path20"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:43.5655365px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62478876px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1"
+             d="m 196.72319,63.830044 h 6.88336 V 44.138421 h 7.88536 V 38.77986 h -22.65408 v 5.358561 h 7.88536 z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path22"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:43.5655365px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62478876px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1"
+             d="m 243.35874,38.77986 -5.48925,16.685601 -5.315,-16.685601 h -6.36057 l -5.48926,16.380642 -5.27143,-16.380642 h -7.10118 l 8.32102,25.050184 h 7.36257 l 5.22787,-15.857855 5.09717,15.857855 h 7.36257 L 249.9807,38.77986 Z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path24"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:43.5655365px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62478876px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1"
+             d="m 261.84822,64.309265 c 8.10319,0 14.07167,-5.489258 14.07167,-12.98253 0,-7.536838 -5.96848,-13.026096 -14.07167,-13.026096 -8.10319,0 -14.07166,5.489258 -14.07166,13.026096 0,7.493272 5.96847,12.98253 14.07166,12.98253 z m 0,-5.619955 c -4.09516,0 -7.14474,-3.093153 -7.14474,-7.362575 0,-4.312988 3.04958,-7.406141 7.14474,-7.406141 4.09516,0 7.10119,3.093153 7.10119,7.406141 0,4.269422 -3.00603,7.362575 -7.10119,7.362575 z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path26"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:43.5655365px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62478876px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1"
+             d="m 292.97988,63.830044 h 7.40615 l -5.57639,-7.885362 c 3.22385,-1.481229 5.09717,-4.225857 5.09717,-7.928928 0,-5.794216 -4.40012,-9.235894 -11.28348,-9.235894 h -11.41417 v 25.050184 h 6.88336 v -6.709093 h 4.22585 z m 0,-15.81429 c 0,2.43967 -1.61192,3.877333 -4.83577,3.877333 h -4.05159 v -7.754666 h 4.05159 c 3.22385,0 4.83577,1.394097 4.83577,3.877333 z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path28"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:43.5655365px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62478876px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1"
+             d="m 318.78498,63.830044 h 8.01605 L 315.82252,49.932638 326.23468,38.77986 h -7.5804 L 308.45994,49.453417 V 38.77986 h -6.79622 v 25.050184 h 6.79622 v -6.317003 l 2.83176,-2.918891 z" />
+        </g>
+        <g
+           id="text1546"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.33208466px;line-height:1.25;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.02106726"
+           aria-label="Manager">
+          <path
+             inkscape:connector-curvature="0"
+             id="path31"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.88482285px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.45056129px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1"
+             d="m 172.951,105.36586 -0.0858,-30.019376 h -5.70368 L 156.09727,94.001382 144.86144,75.346484 h -5.74656 v 30.019376 h 6.51849 V 87.740198 l 8.79139,14.452182 h 3.13059 l 8.83427,-14.838146 0.0429,18.011626 z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path33"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.88482285px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.45056129px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1"
+             d="m 195.84872,105.36586 h 6.69003 L 191.43158,80.707087 h -6.69003 l -11.06428,24.658773 h 6.56137 l 2.05847,-4.88887 h 11.53602 z m -11.49313,-9.77774 3.73098,-8.920043 3.68809,8.920043 z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path35"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.88482285px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.45056129px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1"
+             d="M 219.91371,80.707087 V 94.344461 L 208.54923,80.707087 h -5.57503 v 24.658773 h 6.64715 V 91.771371 l 11.36448,13.594489 h 5.57503 V 80.707087 Z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path37"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.88482285px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.45056129px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1"
+             d="m 249.16039,105.36586 h 6.69003 L 244.74325,80.707087 h -6.69003 l -11.06428,24.658773 h 6.56138 l 2.05847,-4.88887 h 11.53601 z m -11.49313,-9.77774 3.73098,-8.920043 3.68809,8.920043 z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path39"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.88482285px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.45056129px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1"
+             d="m 272.07754,99.533524 c -1.20077,0.514616 -2.44443,0.771926 -3.85963,0.771926 -4.37425,0 -7.33331,-2.959052 -7.33331,-7.247534 0,-4.374252 3.04483,-7.29042 7.24754,-7.29042 2.44443,0 4.54579,0.900581 6.2183,2.873283 l 4.46002,-3.988288 c -2.44443,-2.959053 -5.96099,-4.417137 -10.76409,-4.417137 -8.10523,0 -13.98045,5.403487 -13.98045,12.822562 0,7.376194 5.74656,12.779674 14.02333,12.779674 3.47368,0 7.41908,-1.115 10.16371,-3.13059 v -9.992163 h -6.17542 z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path41"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.88482285px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.45056129px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1"
+             d="M 288.33347,100.1768 V 95.330811 H 299.5693 V 90.399057 H 288.33347 V 85.89615 h 12.69391 v -5.189063 h -19.38394 v 24.658773 h 19.85567 v -5.18906 z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path43"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.88482285px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.45056129px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1"
+             d="m 319.50997,105.36586 h 7.29042 l -5.48926,-7.762153 c 3.17348,-1.458084 5.01753,-4.159828 5.01753,-7.805038 0,-5.703681 -4.33137,-9.091582 -11.10717,-9.091582 h -11.23583 v 24.658773 h 6.77581 v -6.604263 h 4.15982 z m 0,-15.567191 c 0,2.40155 -1.58674,3.81675 -4.76022,3.81675 h -3.98828 V 85.98192 h 3.98828 c 3.17348,0 4.76022,1.372314 4.76022,3.816749 z" />
+        </g>
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/contrib/art/logo/stickers/nm_sticker_red.svg b/contrib/art/logo/stickers/nm_sticker_red.svg
new file mode 100644
index 00000000..702f3c76
--- /dev/null
+++ b/contrib/art/logo/stickers/nm_sticker_red.svg
@@ -0,0 +1,180 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="47.889584mm"
+   height="54.9034mm"
+   viewBox="0 0 47.889584 54.9034"
+   version="1.1"
+   id="svg875"
+   inkscape:version="0.92.4 (unknown)"
+   sodipodi:docname="nm_sticker03.svg">
+  <defs
+     id="defs869" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.98994949"
+     inkscape:cx="-343.27723"
+     inkscape:cy="-18.266293"
+     inkscape:document-units="mm"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     inkscape:window-width="1920"
+     inkscape:window-height="1136"
+     inkscape:window-x="1280"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata872">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-81.055209,-121.0483)">
+    <polygon
+       transform="matrix(0.26458333,0,0,0.26458333,81.055209,120.85104)"
+       id="Polygon-1"
+       points="-1.5827339e-12,52.622771 90.5,0.74554102 181,52.622771 181,156.37723 90.5,208.25446 -1.5258905e-12,156.37723 "
+       style="fill:#bb0000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1" />
+    <polygon
+       transform="matrix(0.26458333,0,0,0.26458333,81.055209,120.85104)"
+       id="Polygon-1-Copy"
+       points="11.810617,149.56493 11.810617,59.435072 90.5,14.370145 169.18938,59.435072 169.18938,149.56493 90.5,194.62986 "
+       style="fill:#cc0000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1"
+       inkscape:label="Polygon-2" />
+    <g
+       id="g981"
+       transform="matrix(1.0214286,0,0,1.0214286,-2.3225275,-2.3090111)">
+      <g
+         inkscape:label="logo"
+         style="stroke-width:0.87496203"
+         transform="matrix(0.00426687,0,0,0.00426687,74.275548,131.38514)"
+         id="g1538">
+        <path
+           sodipodi:nodetypes="ccccccccccccccccccccccccc"
+           style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.43124211"
+           d="M 5029.9666,-215.25215 V 4157.9482 h 853.3745 V 1209.6083 l 627.4666,630.5285 -296.1503,297.5952 828.9187,833.0503 296.5727,-298.8683 1478.8184,1486.0342 1.6018,1.6095 1.6016,-1.6095 h 582.6059 V -215.24452 H 8820.5686 L 7490.5865,1121.2273 7358.5311,988.27476 7096.6167,1251.4682 5651.6129,-215.25215 Z M 8577.4587,1206.4725 v 1531.3676 l -651.8359,-655.0173 260.2277,-261.4989 -110.0444,-110.8354 z"
+           id="path1534"
+           inkscape:connector-curvature="0" />
+        <path
+           sodipodi:nodetypes="ccccccccccccccccccccc"
+           style="fill:#cc0000;fill-opacity:1;stroke:none;stroke-width:1.43124211"
+           d="m 7360.1336,1196.5577 -935.0009,939.5654 108.7309,111.5086 248.8436,-250.0604 42.4166,44.3192 44.1019,42.6221 -248.8434,250.0585 77.4949,77.8737 248.0001,-249.2123 42.5016,44.2339 44.0169,42.6214 -236.8198,258.2846 69.9416,70.285 248.0871,-249.2115 42.4169,44.2339 44.0168,42.7063 -248.0007,249.2114 94.8286,95.2918 935,-939.5654 -311.6662,-311.577 z"
+           id="path1536"
+           inkscape:connector-curvature="0" />
+      </g>
+      <g
+         inkscape:label="g1548"
+         id="g1548"
+         transform="matrix(0.14918522,0,0,0.14918522,70.31712,146.41059)"
+         style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.02106726;image-rendering:auto">
+        <g
+           id="text1542"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.33208466px;line-height:1.25;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62478876px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.02106726"
+           aria-label="Network">
+          <path
+             inkscape:connector-curvature="0"
+             id="path16"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:43.5655365px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62478876px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1"
+             d="M 160.11453,33.334168 V 51.849521 L 144.95372,33.334168 h -5.83778 v 30.495876 h 6.97049 V 45.314691 l 15.20437,18.515353 h 5.79421 V 33.334168 Z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path18"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:43.5655365px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62478876px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1"
+             d="m 177.2528,58.558614 v -4.922906 h 11.41417 V 48.625672 H 177.2528 V 44.05129 h 12.8954 v -5.27143 h -19.69162 v 25.050184 h 20.17084 v -5.27143 z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path20"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:43.5655365px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62478876px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1"
+             d="m 196.72319,63.830044 h 6.88336 V 44.138421 h 7.88536 V 38.77986 h -22.65408 v 5.358561 h 7.88536 z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path22"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:43.5655365px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62478876px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1"
+             d="m 243.35874,38.77986 -5.48925,16.685601 -5.315,-16.685601 h -6.36057 l -5.48926,16.380642 -5.27143,-16.380642 h -7.10118 l 8.32102,25.050184 h 7.36257 l 5.22787,-15.857855 5.09717,15.857855 h 7.36257 L 249.9807,38.77986 Z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path24"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:43.5655365px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62478876px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1"
+             d="m 261.84822,64.309265 c 8.10319,0 14.07167,-5.489258 14.07167,-12.98253 0,-7.536838 -5.96848,-13.026096 -14.07167,-13.026096 -8.10319,0 -14.07166,5.489258 -14.07166,13.026096 0,7.493272 5.96847,12.98253 14.07166,12.98253 z m 0,-5.619955 c -4.09516,0 -7.14474,-3.093153 -7.14474,-7.362575 0,-4.312988 3.04958,-7.406141 7.14474,-7.406141 4.09516,0 7.10119,3.093153 7.10119,7.406141 0,4.269422 -3.00603,7.362575 -7.10119,7.362575 z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path26"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:43.5655365px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62478876px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1"
+             d="m 292.97988,63.830044 h 7.40615 l -5.57639,-7.885362 c 3.22385,-1.481229 5.09717,-4.225857 5.09717,-7.928928 0,-5.794216 -4.40012,-9.235894 -11.28348,-9.235894 h -11.41417 v 25.050184 h 6.88336 v -6.709093 h 4.22585 z m 0,-15.81429 c 0,2.43967 -1.61192,3.877333 -4.83577,3.877333 h -4.05159 v -7.754666 h 4.05159 c 3.22385,0 4.83577,1.394097 4.83577,3.877333 z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path28"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:43.5655365px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62478876px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1"
+             d="m 318.78498,63.830044 h 8.01605 L 315.82252,49.932638 326.23468,38.77986 h -7.5804 L 308.45994,49.453417 V 38.77986 h -6.79622 v 25.050184 h 6.79622 v -6.317003 l 2.83176,-2.918891 z" />
+        </g>
+        <g
+           id="text1546"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.33208466px;line-height:1.25;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.02106726"
+           aria-label="Manager">
+          <path
+             inkscape:connector-curvature="0"
+             id="path31"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.88482285px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.45056129px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1"
+             d="m 172.951,105.36586 -0.0858,-30.019376 h -5.70368 L 156.09727,94.001382 144.86144,75.346484 h -5.74656 v 30.019376 h 6.51849 V 87.740198 l 8.79139,14.452182 h 3.13059 l 8.83427,-14.838146 0.0429,18.011626 z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path33"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.88482285px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.45056129px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1"
+             d="m 195.84872,105.36586 h 6.69003 L 191.43158,80.707087 h -6.69003 l -11.06428,24.658773 h 6.56137 l 2.05847,-4.88887 h 11.53602 z m -11.49313,-9.77774 3.73098,-8.920043 3.68809,8.920043 z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path35"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.88482285px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.45056129px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1"
+             d="M 219.91371,80.707087 V 94.344461 L 208.54923,80.707087 h -5.57503 v 24.658773 h 6.64715 V 91.771371 l 11.36448,13.594489 h 5.57503 V 80.707087 Z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path37"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.88482285px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.45056129px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1"
+             d="m 249.16039,105.36586 h 6.69003 L 244.74325,80.707087 h -6.69003 l -11.06428,24.658773 h 6.56138 l 2.05847,-4.88887 h 11.53601 z m -11.49313,-9.77774 3.73098,-8.920043 3.68809,8.920043 z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path39"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.88482285px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.45056129px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1"
+             d="m 272.07754,99.533524 c -1.20077,0.514616 -2.44443,0.771926 -3.85963,0.771926 -4.37425,0 -7.33331,-2.959052 -7.33331,-7.247534 0,-4.374252 3.04483,-7.29042 7.24754,-7.29042 2.44443,0 4.54579,0.900581 6.2183,2.873283 l 4.46002,-3.988288 c -2.44443,-2.959053 -5.96099,-4.417137 -10.76409,-4.417137 -8.10523,0 -13.98045,5.403487 -13.98045,12.822562 0,7.376194 5.74656,12.779674 14.02333,12.779674 3.47368,0 7.41908,-1.115 10.16371,-3.13059 v -9.992163 h -6.17542 z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path41"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.88482285px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.45056129px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1"
+             d="M 288.33347,100.1768 V 95.330811 H 299.5693 V 90.399057 H 288.33347 V 85.89615 h 12.69391 v -5.189063 h -19.38394 v 24.658773 h 19.85567 v -5.18906 z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path43"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.88482285px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.45056129px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1"
+             d="m 319.50997,105.36586 h 7.29042 l -5.48926,-7.762153 c 3.17348,-1.458084 5.01753,-4.159828 5.01753,-7.805038 0,-5.703681 -4.33137,-9.091582 -11.10717,-9.091582 h -11.23583 v 24.658773 h 6.77581 v -6.604263 h 4.15982 z m 0,-15.567191 c 0,2.40155 -1.58674,3.81675 -4.76022,3.81675 h -3.98828 V 85.98192 h 3.98828 c 3.17348,0 4.76022,1.372314 4.76022,3.816749 z" />
+        </g>
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/contrib/art/logo/stickers/nm_sticker_white.svg b/contrib/art/logo/stickers/nm_sticker_white.svg
new file mode 100644
index 00000000..5a29e431
--- /dev/null
+++ b/contrib/art/logo/stickers/nm_sticker_white.svg
@@ -0,0 +1,180 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="47.889584mm"
+   height="54.9034mm"
+   viewBox="0 0 47.889584 54.9034"
+   version="1.1"
+   id="svg875"
+   inkscape:version="0.92.4 (unknown)"
+   sodipodi:docname="nm_sticker01.svg">
+  <defs
+     id="defs869" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.98994949"
+     inkscape:cx="-145.1366"
+     inkscape:cy="-4.9536298"
+     inkscape:document-units="mm"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     inkscape:window-width="1920"
+     inkscape:window-height="1136"
+     inkscape:window-x="1280"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata872">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-81.055209,-121.0483)">
+    <polygon
+       transform="matrix(0.26458333,0,0,0.26458333,81.055209,120.85104)"
+       id="Polygon-1"
+       points="-1.5258905e-12,156.37723 -1.5827339e-12,52.622771 90.5,0.74554102 181,52.622771 181,156.37723 90.5,208.25446 "
+       style="fill:#32557d;fill-rule:evenodd;stroke:none;stroke-width:1;fill-opacity:1" />
+    <polygon
+       transform="matrix(0.26458333,0,0,0.26458333,81.055209,120.85104)"
+       id="Polygon-1-Copy"
+       points="90.5,194.62986 11.810617,149.56493 11.810617,59.435072 90.5,14.370145 169.18938,59.435072 169.18938,149.56493 "
+       style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:1"
+       inkscape:label="Polygon-2" />
+    <g
+       id="g981"
+       transform="matrix(1.0214286,0,0,1.0214286,-2.3225275,-2.3090111)">
+      <g
+         inkscape:label="logo"
+         style="stroke-width:0.87496203"
+         transform="matrix(0.00426687,0,0,0.00426687,74.275548,131.38514)"
+         id="g1538">
+        <path
+           sodipodi:nodetypes="ccccccccccccccccccccccccc"
+           style="fill:#32557d;fill-opacity:1;stroke:none;stroke-width:1.43124211"
+           d="M 5029.9666,-215.25215 V 4157.9482 h 853.3745 V 1209.6083 l 627.4666,630.5285 -296.1503,297.5952 828.9187,833.0503 296.5727,-298.8683 1478.8184,1486.0342 1.6018,1.6095 1.6016,-1.6095 h 582.6059 V -215.24452 H 8820.5686 L 7490.5865,1121.2273 7358.5311,988.27476 7096.6167,1251.4682 5651.6129,-215.25215 Z M 8577.4587,1206.4725 v 1531.3676 l -651.8359,-655.0173 260.2277,-261.4989 -110.0444,-110.8354 z"
+           id="path1534"
+           inkscape:connector-curvature="0" />
+        <path
+           sodipodi:nodetypes="ccccccccccccccccccccc"
+           style="fill:#ffffff;stroke:none;stroke-width:1.43124211"
+           d="m 7360.1336,1196.5577 -935.0009,939.5654 108.7309,111.5086 248.8436,-250.0604 42.4166,44.3192 44.1019,42.6221 -248.8434,250.0585 77.4949,77.8737 248.0001,-249.2123 42.5016,44.2339 44.0169,42.6214 -236.8198,258.2846 69.9416,70.285 248.0871,-249.2115 42.4169,44.2339 44.0168,42.7063 -248.0007,249.2114 94.8286,95.2918 935,-939.5654 -311.6662,-311.577 z"
+           id="path1536"
+           inkscape:connector-curvature="0" />
+      </g>
+      <g
+         inkscape:label="g1548"
+         id="g1548"
+         transform="matrix(0.14918522,0,0,0.14918522,70.31712,146.41059)"
+         style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.02106726;image-rendering:auto">
+        <g
+           id="text1542"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.33208466px;line-height:1.25;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62478876px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.02106726"
+           aria-label="Network">
+          <path
+             inkscape:connector-curvature="0"
+             id="path16"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:43.5655365px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62478876px;writing-mode:lr-tb;text-anchor:start"
+             d="M 160.11453,33.334168 V 51.849521 L 144.95372,33.334168 h -5.83778 v 30.495876 h 6.97049 V 45.314691 l 15.20437,18.515353 h 5.79421 V 33.334168 Z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path18"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:43.5655365px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62478876px;writing-mode:lr-tb;text-anchor:start"
+             d="m 177.2528,58.558614 v -4.922906 h 11.41417 V 48.625672 H 177.2528 V 44.05129 h 12.8954 v -5.27143 h -19.69162 v 25.050184 h 20.17084 v -5.27143 z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path20"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:43.5655365px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62478876px;writing-mode:lr-tb;text-anchor:start"
+             d="m 196.72319,63.830044 h 6.88336 V 44.138421 h 7.88536 V 38.77986 h -22.65408 v 5.358561 h 7.88536 z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path22"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:43.5655365px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62478876px;writing-mode:lr-tb;text-anchor:start"
+             d="m 243.35874,38.77986 -5.48925,16.685601 -5.315,-16.685601 h -6.36057 l -5.48926,16.380642 -5.27143,-16.380642 h -7.10118 l 8.32102,25.050184 h 7.36257 l 5.22787,-15.857855 5.09717,15.857855 h 7.36257 L 249.9807,38.77986 Z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path24"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:43.5655365px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62478876px;writing-mode:lr-tb;text-anchor:start"
+             d="m 261.84822,64.309265 c 8.10319,0 14.07167,-5.489258 14.07167,-12.98253 0,-7.536838 -5.96848,-13.026096 -14.07167,-13.026096 -8.10319,0 -14.07166,5.489258 -14.07166,13.026096 0,7.493272 5.96847,12.98253 14.07166,12.98253 z m 0,-5.619955 c -4.09516,0 -7.14474,-3.093153 -7.14474,-7.362575 0,-4.312988 3.04958,-7.406141 7.14474,-7.406141 4.09516,0 7.10119,3.093153 7.10119,7.406141 0,4.269422 -3.00603,7.362575 -7.10119,7.362575 z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path26"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:43.5655365px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62478876px;writing-mode:lr-tb;text-anchor:start"
+             d="m 292.97988,63.830044 h 7.40615 l -5.57639,-7.885362 c 3.22385,-1.481229 5.09717,-4.225857 5.09717,-7.928928 0,-5.794216 -4.40012,-9.235894 -11.28348,-9.235894 h -11.41417 v 25.050184 h 6.88336 v -6.709093 h 4.22585 z m 0,-15.81429 c 0,2.43967 -1.61192,3.877333 -4.83577,3.877333 h -4.05159 v -7.754666 h 4.05159 c 3.22385,0 4.83577,1.394097 4.83577,3.877333 z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path28"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:43.5655365px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-3.62478876px;writing-mode:lr-tb;text-anchor:start"
+             d="m 318.78498,63.830044 h 8.01605 L 315.82252,49.932638 326.23468,38.77986 h -7.5804 L 308.45994,49.453417 V 38.77986 h -6.79622 v 25.050184 h 6.79622 v -6.317003 l 2.83176,-2.918891 z" />
+        </g>
+        <g
+           id="text1546"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.33208466px;line-height:1.25;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.02106726"
+           aria-label="Manager">
+          <path
+             inkscape:connector-curvature="0"
+             id="path31"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.88482285px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.45056129px;writing-mode:lr-tb;text-anchor:start"
+             d="m 172.951,105.36586 -0.0858,-30.019376 h -5.70368 L 156.09727,94.001382 144.86144,75.346484 h -5.74656 v 30.019376 h 6.51849 V 87.740198 l 8.79139,14.452182 h 3.13059 l 8.83427,-14.838146 0.0429,18.011626 z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path33"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.88482285px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.45056129px;writing-mode:lr-tb;text-anchor:start"
+             d="m 195.84872,105.36586 h 6.69003 L 191.43158,80.707087 h -6.69003 l -11.06428,24.658773 h 6.56137 l 2.05847,-4.88887 h 11.53602 z m -11.49313,-9.77774 3.73098,-8.920043 3.68809,8.920043 z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path35"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.88482285px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.45056129px;writing-mode:lr-tb;text-anchor:start"
+             d="M 219.91371,80.707087 V 94.344461 L 208.54923,80.707087 h -5.57503 v 24.658773 h 6.64715 V 91.771371 l 11.36448,13.594489 h 5.57503 V 80.707087 Z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path37"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.88482285px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.45056129px;writing-mode:lr-tb;text-anchor:start"
+             d="m 249.16039,105.36586 h 6.69003 L 244.74325,80.707087 h -6.69003 l -11.06428,24.658773 h 6.56138 l 2.05847,-4.88887 h 11.53601 z m -11.49313,-9.77774 3.73098,-8.920043 3.68809,8.920043 z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path39"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.88482285px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.45056129px;writing-mode:lr-tb;text-anchor:start"
+             d="m 272.07754,99.533524 c -1.20077,0.514616 -2.44443,0.771926 -3.85963,0.771926 -4.37425,0 -7.33331,-2.959052 -7.33331,-7.247534 0,-4.374252 3.04483,-7.29042 7.24754,-7.29042 2.44443,0 4.54579,0.900581 6.2183,2.873283 l 4.46002,-3.988288 c -2.44443,-2.959053 -5.96099,-4.417137 -10.76409,-4.417137 -8.10523,0 -13.98045,5.403487 -13.98045,12.822562 0,7.376194 5.74656,12.779674 14.02333,12.779674 3.47368,0 7.41908,-1.115 10.16371,-3.13059 v -9.992163 h -6.17542 z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path41"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.88482285px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.45056129px;writing-mode:lr-tb;text-anchor:start"
+             d="M 288.33347,100.1768 V 95.330811 H 299.5693 V 90.399057 H 288.33347 V 85.89615 h 12.69391 v -5.189063 h -19.38394 v 24.658773 h 19.85567 v -5.18906 z" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path43"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.88482285px;font-family:Montserrat;-inkscape-font-specification:'Montserrat, Bold';font-variant-ligatures:normal;font-variant-caps:small-caps;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-2.45056129px;writing-mode:lr-tb;text-anchor:start"
+             d="m 319.50997,105.36586 h 7.29042 l -5.48926,-7.762153 c 3.17348,-1.458084 5.01753,-4.159828 5.01753,-7.805038 0,-5.703681 -4.33137,-9.091582 -11.10717,-9.091582 h -11.23583 v 24.658773 h 6.77581 v -6.604263 h 4.15982 z m 0,-15.567191 c 0,2.40155 -1.58674,3.81675 -4.76022,3.81675 h -3.98828 V 85.98192 h 3.98828 c 3.17348,0 4.76022,1.372314 4.76022,3.816749 z" />
+        </g>
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/contrib/debian/REQUIRED_PACKAGES b/contrib/debian/REQUIRED_PACKAGES
new file mode 100755
index 00000000..8e68bba3
--- /dev/null
+++ b/contrib/debian/REQUIRED_PACKAGES
@@ -0,0 +1,106 @@
+#!/bin/bash
+
+set -xe
+
+# A list of packages useful/needed to build and develop
+# NetworkManager on Debian and Ubuntu.
+#
+# Not all of these packages are available, depending
+# on your distribution/release. But the script will happily
+# skip them.
+#
+# Not all of these packages are strictly speaking necessary.
+# This is a generous list of related packages.
+
+SUDO=
+[ "$EUID" -eq 0 ] || SUDO=sudo
+
+install() {
+    if [ "$NM_INSTALL" != "" ]; then
+        $NM_INSTALL "$@"
+    else
+        $SUDO apt-get install -y "$@"
+    fi
+}
+
+install_ignore_missing() {
+    for p; do
+        install "$p" || :
+    done
+}
+
+
+install \
+    \
+    autoconf \
+    automake \
+    autopoint \
+    clang \
+    dbus \
+    dbus-x11 \
+    dnsmasq \
+    git \
+    gobject-introspection \
+    gtk-doc-tools \
+    intltool \
+    iproute2 \
+    iptables \
+    libaudit-dev \
+    libcurl4-gnutls-dev \
+    libdbus-1-dev \
+    libgirepository1.0-dev \
+    libglib2.0-dev \
+    libglib2.0-doc \
+    libgnutls28-dev \
+    libiw-dev \
+    libjansson-dev \
+    libjansson4 \
+    libmm-glib-dev \
+    libndp-dev \
+    libnewt-dev \
+    libnss3-dev \
+    libpolkit-gobject-1-dev \
+    libpsl-dev \
+    libreadline-dev \
+    libsystemd-dev \
+    libteam-dev \
+    libtool \
+    libudev-dev \
+    locales \
+    make \
+    meson \
+    mobile-broadband-provider-info \
+    pkg-config \
+    policykit-1 \
+    ppp \
+    ppp-dev \
+    python3-dbus \
+    python3-gi \
+    python3-pip \
+    python3-setuptools \
+    python3-pexpect \
+    udev \
+    uuid-dev \
+    valgrind \
+    \
+    #end
+
+install_ignore_missing \
+    python-setuptools \
+    \
+    #end
+
+# Old. Present in Ubuntu 20.04, not in Ubuntu 22.04
+# software-properties-common
+install_ignore_missing \
+    python-dbus \
+    python-gi \
+    \
+    #end
+
+# Old. Present in Ubuntu 18.04, not in Ubuntu 20.04
+# software-properties-common
+install_ignore_missing \
+    libgcrypt11-dev \
+    \
+    #end
diff --git a/contrib/editors/networkmanager-style.el b/contrib/editors/networkmanager-style.el
new file mode 100644
index 00000000..69c7ac5e
--- /dev/null
+++ b/contrib/editors/networkmanager-style.el
@@ -0,0 +1,61 @@
+;;; Emacs support for hacking on NetworkManager
+
+(c-add-style "NetworkManager"
+             '(
+               ; Start with the "bsd" style
+               "bsd"
+
+               ; ...but remove the rule saying labels must be indented at
+               ; least one space
+               (c-label-minimum-indentation . 0)
+
+               ; 4-space tabs/indents
+               (tab-width . 4)
+               (c-basic-offset . 4)
+
+               ; Use smart-tabs-mode (see below) to get tabs for indentation
+               ; but spaces for alignment of continuation lines.
+               (smart-tabs-mode . t)
+
+               ; Multi-line "if" conditions are indented like this:
+               ;     if (   foo
+               ;         && bar)
+               ; (You have to add the spaces on the first line yourself, but
+               ; this will make emacs align the "&&" correctly.)
+               (c-offsets-alist (arglist-cont-nonempty . (nm-lineup-arglist))
+                                (arglist-close . (nm-lineup-arglist)))
+
+               ; NM's comments use two spaces after a period and are
+               ; (generally) wrapped at 80 characters
+               (sentence-end-double-space . t)
+               (fill-column . 80)
+               ))
+
+;; http://www.emacswiki.org/emacs/SmartTabs
+(require 'smart-tabs-mode)
+
+;; The smart-tabs-mode documentation tells you to use
+;; smart-tabs-insinuate to set it up, but that will cause it to be
+;; enabled for *all* C code. We only want to enable it for
+;; NetworkManager, so we have to manually set it up first.
+(smart-tabs-advice c-indent-line c-basic-offset)
+(smart-tabs-advice c-indent-region c-basic-offset)
+
+
+;; Implements the weird "if" alignment
+(defun nm-lineup-arglist (langelem)
+  (save-excursion
+    (back-to-indentation)
+    (c-go-up-list-backward)
+    (vector (+ (current-column) 1))))
+
+
+(dir-locals-set-class-variables 'nm '((c-mode . ((c-file-style . "NetworkManager")))))
+
+;; Now add a line like the following for every directory where you want the
+;; "NetworkManager" style to be the default
+
+; (dir-locals-set-directory-class "/home/danw/gnome/NetworkManager/" 'nm)
+; (dir-locals-set-directory-class "/home/danw/gnome/network-manager-applet/" 'nm)
+
+(provide 'networkmanager-style)
diff --git a/contrib/fedora/REQUIRED_PACKAGES b/contrib/fedora/REQUIRED_PACKAGES
new file mode 100755
index 00000000..38ca9026
--- /dev/null
+++ b/contrib/fedora/REQUIRED_PACKAGES
@@ -0,0 +1,108 @@
+#!/bin/bash
+
+# A list of packages useful/needed to build and develop
+# NetworkManager on Fedora and RHEL.
+#
+# Not all of these packages are available, depending
+# on your distribution/release. But yum will happily
+# skip them.
+#
+# Not all of these packages are strictly speaking necessary.
+# This is a generous list of related packages.
+
+set -xe
+
+DNF="$(command -v dnf &>/dev/null && echo dnf || echo yum)"
+
+SUDO=
+[ "$EUID" -eq 0 ] || SUDO=sudo
+
+install() {
+    if [ "$NM_INSTALL" != "" ]; then
+        $NM_INSTALL "$@"
+    else
+        $SUDO "$DNF" install -y "$@"
+    fi
+}
+
+install_ignore_missing() {
+    for p; do
+        install "$p" || :
+    done
+}
+
+if test "$NM_NO_EXTRA" != 1; then
+    # these packages are convenient for developing, but not necessary
+    # for CI testing.
+    EXTRA_PACKAGES=(
+        bash-completion \
+        cscope \
+    )
+else
+    EXTRA_PACKAGES=()
+fi
+
+install \
+    /usr/bin/clang-format \
+    /usr/bin/xargs \
+    ModemManager-devel \
+    ModemManager-glib-devel \
+    audit-libs-devel \
+    bluez-libs-devel \
+    clang \
+    dbus-devel \
+    dbus-x11 \
+    dhclient \
+    firewalld-filesystem \
+    gcc-c++ \
+    gettext-devel \
+    git \
+    glib2-doc \
+    gnutls-devel \
+    gobject-introspection-devel \
+    gtk-doc \
+    iptables \
+    jansson-devel \
+    jq \
+    libcurl-devel \
+    libndp-devel \
+    libselinux-devel \
+    libtool \
+    libuuid-devel \
+    make \
+    meson \
+    mobile-broadband-provider-info-devel \
+    newt-devel \
+    nss-devel \
+    polkit-devel \
+    ppp \
+    ppp-devel \
+    python3-dbus \
+    python3-gobject \
+    python3-pexpect \
+    readline-devel \
+    rpm-build \
+    systemd-devel \
+    teamd-devel \
+    util-linux \
+    vala \
+    valgrind \
+    which \
+    "${EXTRA_PACKAGES[@]}"
+
+# some packages don't exist in certain distributions. Install them one-by-one, and ignore errors.
+install_ignore_missing \
+    black \
+    dbus-python \
+    iproute-tc \
+    libasan \
+    libpsl-devel \
+    libubsan \
+    libvala-devel \
+    pexpect \
+    pygobject3-base \
+    python-gobject-base \
+    python36-pexpect \
+    qt-devel \
+    vala-devel \
+    #end
diff --git a/contrib/fedora/rpm/00-server.conf b/contrib/fedora/rpm/00-server.conf
new file mode 100644
index 00000000..ba3d29fb
--- /dev/null
+++ b/contrib/fedora/rpm/00-server.conf
@@ -0,0 +1,14 @@
+# This configuration file changes NetworkManager's behavior to
+# what's expected on "traditional UNIX server" type deployments.
+#
+# See "man NetworkManager.conf" for more information about these
+# and other keys.
+
+[main]
+# Do not do automatic (DHCP/SLAAC) configuration on ethernet devices
+# with no other matching connections.
+no-auto-default=*
+
+# Ignore the carrier (cable plugged in) state when attempting to
+# activate static-IP connections.
+ignore-carrier=*
diff --git a/contrib/fedora/rpm/20-connectivity-fedora.conf b/contrib/fedora/rpm/20-connectivity-fedora.conf
new file mode 100644
index 00000000..5c9476a0
--- /dev/null
+++ b/contrib/fedora/rpm/20-connectivity-fedora.conf
@@ -0,0 +1,10 @@
+# Enable connectivity checking for NetworkManager.
+# See `man NetworkManager.conf`.
+#
+# Note that connectivity checking works badly with rp_filter set to
+# strict. Check "/proc/sys/net/ipv4/conf/*/rp_filter".
+[connectivity]
+enabled=true
+uri=http://fedoraproject.org/static/hotspot.txt
+response=OK
+interval=300
diff --git a/contrib/fedora/rpm/20-connectivity-redhat.conf b/contrib/fedora/rpm/20-connectivity-redhat.conf
new file mode 100644
index 00000000..070f030b
--- /dev/null
+++ b/contrib/fedora/rpm/20-connectivity-redhat.conf
@@ -0,0 +1,10 @@
+# Enable connectivity checking for NetworkManager.
+# See `man NetworkManager.conf`.
+#
+# Note that connectivity checking works badly with rp_filter set to
+# strict. Check "/proc/sys/net/ipv4/conf/*/rp_filter".
+[connectivity]
+enabled=true
+uri=http://static.redhat.com/test/rhel-networkmanager.txt
+response=OK
+interval=300
diff --git a/contrib/fedora/rpm/22-wifi-mac-addr.conf b/contrib/fedora/rpm/22-wifi-mac-addr.conf
new file mode 100644
index 00000000..2e329c88
--- /dev/null
+++ b/contrib/fedora/rpm/22-wifi-mac-addr.conf
@@ -0,0 +1,31 @@
+# This sets defaults for Wi-Fi profiles to set a generated, stable MAC address.
+#
+# Do not modify this file. You can hide/overwrite this file by placing a file
+# to "/etc/NetworkManager/conf.d/22-wifi-mac-addr.conf". You can also add
+# configuration snippets with higher priority that override this setting (see
+# `man 5 NetworkManager.conf`). Most importantly, this snippet only sets
+# default values for the profile. You can explicitly set the value for each
+# profile, so that this default value is not used.
+#
+# For example, on a particular profile/network set
+#
+#   $ nmcli connection modify "$PROFILE" wifi.cloned-mac-address permanent
+#
+# to use the hardware MAC address. This prevents the default from this file
+# to take effect.
+#
+# Or
+#
+#   $ nmcli connection modify "$PROFILE" wifi.cloned-mac-address stable connection.stable-id '${NETWORK_SSID}/${BOOT}'
+#
+# to get a generated MAC address that changes on each boot. Note how setting
+# "connection.stable-id" also affects other aspects of the profile.
+#
+# See `man 5 nm-settings` for "wifi.cloned-mac-address" and "connection.stable-id".
+
+[connection.22-wifi-mac-addr]
+match-device=type:wifi
+wifi.cloned-mac-address=stable-ssid
+
+[.config]
+enable=nm-version-min:1.45
diff --git a/contrib/fedora/rpm/70-nm-connectivity.conf b/contrib/fedora/rpm/70-nm-connectivity.conf
new file mode 100644
index 00000000..0e4b0e27
--- /dev/null
+++ b/contrib/fedora/rpm/70-nm-connectivity.conf
@@ -0,0 +1,15 @@
+# The Strict mode of RFC3704 Reverse Path filtering breaks some pretty
+# common and reasonable use cases.
+#
+# Notably, it makes it impossible for NetworkManager to do connectivity
+# check on a newly arriving default route (it starts with a higher metric
+# and is bumped lower if there's connectivity).
+#
+# Kernel's default is 0 (no filter), systemd configures a Loose filter since
+# commit 230450d4e4f1 ('sysctl.d: switch net.ipv4.conf.all.rp_filter from 1
+# to 2'). However, RHEL systemd package happens to default to Strict mode
+# for historic reasons. Let's override it if we're doing connectivity
+# checking.
+
+# Source route verification
+net.ipv4.conf.all.rp_filter = 0
diff --git a/contrib/fedora/rpm/NetworkManager.conf b/contrib/fedora/rpm/NetworkManager.conf
new file mode 100644
index 00000000..287c9d01
--- /dev/null
+++ b/contrib/fedora/rpm/NetworkManager.conf
@@ -0,0 +1,52 @@
+# Configuration file for NetworkManager.
+#
+# See "man 5 NetworkManager.conf" for details.
+#
+# The directories /usr/lib/NetworkManager/conf.d/ and /run/NetworkManager/conf.d/
+# can contain additional .conf snippets installed by packages. These files are
+# read before NetworkManager.conf and have thus lowest priority.
+# The directory /etc/NetworkManager/conf.d/ can contain additional .conf
+# snippets. Those snippets are merged last and overwrite the settings from this main
+# file.
+#
+# The files within one conf.d/ directory are read in asciibetical order.
+#
+# You can prevent loading a file /usr/lib/NetworkManager/conf.d/NAME.conf
+# by having a file NAME.conf in either /run/NetworkManager/conf.d/ or /etc/NetworkManager/conf.d/.
+# Likewise, snippets from /run can be prevented from loading by placing
+# a file with the same name in /etc/NetworkManager/conf.d/.
+#
+# If two files define the same key, the one that is read afterwards will overwrite
+# the previous one.
+
+[main]
+#plugins=keyfile,ifcfg-rh
+
+
+[logging]
+# When debugging NetworkManager, enabling debug logging is of great help.
+#
+# Logfiles contain no passwords and little sensitive information. But please
+# check before posting the file online. You can also personally hand over the
+# logfile to a NM developer to treat it confidential. Meet us on #nm on Libera.Chat.
+#
+# You can also change the log-level at runtime via
+#   $ nmcli general logging level TRACE domains ALL
+# However, usually it's cleaner to enable debug logging
+# in the configuration and restart NetworkManager so that
+# debug logging is enabled from the start.
+#
+# You will find the logfiles in syslog, for example via
+#   $ journalctl -u NetworkManager
+#
+# Please post full logfiles for bug reports without pre-filtering or truncation.
+# Also, for debugging the entire `journalctl` output can be interesting. Don't
+# limit unnecessarily with `journalctl -u`. Exceptions are if you are worried
+# about private data. Check before posting logfiles!
+#
+# Note that debug logging of NetworkManager can be quite verbose. Some messages
+# might be rate-limited by the logging daemon (see RateLimitIntervalSec, RateLimitBurst
+# in man journald.conf). Please disable rate-limiting before collecting debug logs!
+#
+#level=TRACE
+#domains=ALL
diff --git a/contrib/fedora/rpm/NetworkManager.spec b/contrib/fedora/rpm/NetworkManager.spec
new file mode 100644
index 00000000..f55d243d
--- /dev/null
+++ b/contrib/fedora/rpm/NetworkManager.spec
@@ -0,0 +1,1307 @@
+# SPEC file to build NetworkManager for testing. It aims for a similar
+# configuration as rhel-7.0 and Fedora rawhide
+#
+# This spec file is not used as is to create official packages for RHEL, Fedora or any
+# other distribution.
+#
+# Note that it contains __PLACEHOLDERS__ that will be replaced by the accompanying 'build.sh' script.
+
+
+%global wpa_supplicant_version 1:1.1
+
+%global ppp_version %(pkg-config --modversion pppd 2>/dev/null || sed -n 's/^#define\\s*VERSION\\s*"\\([^\\s]*\\)"$/\\1/p' %{_includedir}/pppd/patchlevel.h 2>/dev/null | grep . || echo bad)
+%global glib2_version %(pkg-config --modversion glib-2.0 2>/dev/null || echo bad)
+
+%global epoch_version 1
+%global real_version __VERSION__
+%global rpm_version %{real_version}
+%global release_version __RELEASE_VERSION__
+%global snapshot __SNAPSHOT__
+%global git_sha __COMMIT__
+%global bcond_default_debug __BCOND_DEFAULT_DEBUG__
+%global bcond_default_lto __BCOND_DEFAULT_LTO__
+%global bcond_default_test __BCOND_DEFAULT_TEST__
+
+%global obsoletes_device_plugins     1:0.9.9.95-1
+%global obsoletes_ppp_plugin         1:1.5.3
+%global obsoletes_initscripts_updown 1:1.36.0-0.6
+%global obsoletes_ifcfg_rh           1:1.36.2
+
+%global nmlibdir %{_prefix}/lib/%{name}
+%global nmplugindir %{_libdir}/%{name}/%{version}-%{release}
+
+%global _hardened_build 1
+
+%if "x%{?snapshot}" != "x"
+%global snapshot_dot .%{snapshot}
+%endif
+%if "x%{?git_sha}" != "x"
+%global git_sha_dot .%{git_sha}
+%endif
+
+%global snap %{?snapshot_dot}%{?git_sha_dot}
+
+%global real_version_major %(printf '%s' '%{real_version}' | sed -n 's/^\\([1-9][0-9]*\\.[0-9][0-9]*\\)\\.[0-9][0-9]*$/\\1/p')
+
+%global systemd_units NetworkManager.service NetworkManager-wait-online.service NetworkManager-dispatcher.service nm-priv-helper.service
+
+%global systemd_units_cloud_setup nm-cloud-setup.service nm-cloud-setup.timer
+
+###############################################################################
+%if 0%{?fedora} > 40
+%bcond_without meson
+%else
+%bcond_with    meson
+%endif
+%bcond_without adsl
+%bcond_without bluetooth
+%bcond_without wwan
+%if 0%{?rhel} >= 10
+%bcond_with team
+%else
+%bcond_without team
+%endif
+%bcond_without wifi
+%bcond_without ovs
+%bcond_without ppp
+%bcond_without nmtui
+%bcond_without nm_cloud_setup
+%bcond_without regen_docs
+%if %{bcond_default_debug}
+%bcond_without debug
+%else
+%bcond_with    debug
+%endif
+%if %{bcond_default_test}
+%bcond_without test
+%else
+%bcond_with    test
+%endif
+%if "%{?bcond_default_lto}" == ""
+%if 0%{?fedora} >= 33 || 0%{?rhel} >= 9
+%bcond_without lto
+%else
+%bcond_with    lto
+%endif
+%else
+%if %{bcond_default_lto}
+%bcond_without lto
+%else
+%bcond_with    lto
+%endif
+%endif
+%bcond_with    sanitizer
+%if 0%{?fedora}
+%bcond_without connectivity_fedora
+%else
+%bcond_with connectivity_fedora
+%endif
+%if 0%{?rhel} && 0%{?rhel} >= 8
+%bcond_without connectivity_redhat
+%else
+%bcond_with connectivity_redhat
+%endif
+%if 0%{?fedora} >= 29 || 0%{?rhel} >= 8
+%bcond_without crypto_gnutls
+%else
+%bcond_with crypto_gnutls
+%endif
+%if 0%{?rhel}
+%bcond_with iwd
+%else
+%bcond_without iwd
+%endif
+%if 0%{?fedora} >= 32 || 0%{?rhel} >= 8
+%bcond_without firewalld_zone
+%else
+%bcond_with firewalld_zone
+%endif
+
+###############################################################################
+
+%if 0%{?fedora} || 0%{?rhel} >= 8
+%global dbus_version 1.9.18
+%global dbus_sys_dir %{_datadir}/dbus-1/system.d
+%else
+%global dbus_version 1.1
+%global dbus_sys_dir %{_sysconfdir}/dbus-1/system.d
+%endif
+
+# Older libndp versions use select() (rh#1933041). On well known distros,
+# choose a version that has the necessary fix.
+%if 0%{?rhel} && 0%{?rhel} == 8
+%global libndp_version 1.7-4
+%else
+%global libndp_version %{nil}
+%endif
+
+%if %{with bluetooth} || %{with wwan}
+%global with_modem_manager_1 1
+%else
+%global with_modem_manager_1 0
+%endif
+
+%if 0%{?fedora} >= 31 || 0%{?rhel} >= 8
+%global dhcp_default internal
+%else
+%global dhcp_default dhclient
+%endif
+
+%if 0%{?fedora} || 0%{?rhel} >= 8
+%global logging_backend_default journal
+%if 0%{?fedora} || 0%{?rhel} >= 9
+%global dns_rc_manager_default auto
+%else
+%global dns_rc_manager_default symlink
+%endif
+%else
+%global logging_backend_default syslog
+%global dns_rc_manager_default file
+%endif
+
+%if 0%{?fedora} >= 33 || 0%{?rhel} >= 9
+%global config_plugins_default_ifcfg_rh 0
+%else
+%global config_plugins_default_ifcfg_rh 1
+%endif
+
+%if 0%{?rhel} >= 10
+%global with_ifcfg_rh 0
+%global split_ifcfg_rh 0
+%elif 0%{?fedora} >= 36
+%global with_ifcfg_rh 1
+%global split_ifcfg_rh 1
+%else
+%global with_ifcfg_rh 1
+%global split_ifcfg_rh 0
+%endif
+
+%if (0%{?fedora} >= 36 && 0%{?fedora} < 39) || 0%{?rhel} == 9
+%global ifcfg_warning 1
+%else
+%global ifcfg_warning 0
+%endif
+
+%if 0%{?fedora} >= 39
+%global ifcfg_migrate 1
+%else
+%global ifcfg_migrate 0
+%endif
+
+%if 0%{?fedora}
+# Although eBPF would be available on Fedora's kernel, it seems
+# we often get SELinux denials (rh#1651654). But even aside them,
+# bpf(BPF_MAP_CREATE, ...) randomly fails with EPERM. That might
+# be related to `ulimit -l`. Anyway, this is not usable at the
+# moment.
+%global ebpf_enabled "no"
+%else
+%global ebpf_enabled "no"
+%endif
+
+# Fedora 33 enables LTO by default by setting CFLAGS="-flto -ffat-lto-objects".
+# However, we also require "-flto -flto-partition=none", so disable Fedora's
+# default and use our configure option --with-lto instead.
+%define _lto_cflags %{nil}
+
+###############################################################################
+
+Name: NetworkManager
+Summary: Network connection manager and user applications
+Epoch: %{epoch_version}
+Version: %{rpm_version}
+Release: %{release_version}%{?snap}%{?dist}
+Group: System Environment/Base
+License: GPL-2.0-or-later AND LGPL-2.1-or-later
+URL: https://networkmanager.dev/
+
+#Source: https://download.gnome.org/sources/NetworkManager/%{real_version_major}/%{name}-%{real_version}.tar.xz
+Source: __SOURCE1__
+Source1: NetworkManager.conf
+Source2: 00-server.conf
+Source4: 20-connectivity-fedora.conf
+Source5: 20-connectivity-redhat.conf
+Source6: 22-wifi-mac-addr.conf
+Source7: 70-nm-connectivity.conf
+Source8: readme-ifcfg-rh.txt
+Source9: readme-ifcfg-rh-migrated.txt
+
+#Patch1: 0001-some.patch
+
+Requires(post): systemd
+%if 0%{?fedora} || 0%{?rhel} >= 8
+Requires(post): systemd-udev
+%endif
+Requires(post): /usr/sbin/update-alternatives
+Requires(preun): systemd
+Requires(preun): /usr/sbin/update-alternatives
+Requires(postun): systemd
+
+Requires: dbus >= %{dbus_version}
+Requires: glib2 >= %{glib2_version}
+Requires: %{name}-libnm%{?_isa} = %{epoch}:%{version}-%{release}
+%if "%{libndp_version}" != ""
+Requires: libndp >= %{libndp_version}
+%endif
+Obsoletes: NetworkManager < %{obsoletes_device_plugins}
+Obsoletes: NetworkManager < %{obsoletes_ppp_plugin}
+Obsoletes: NetworkManager-wimax < 1:1.2
+%if 0%{?rhel} && 0%{?rhel} == 8
+Suggests: NetworkManager-initscripts-updown
+%endif
+Obsoletes: NetworkManager < %{obsoletes_initscripts_updown}
+%if 0%{?split_ifcfg_rh}
+Obsoletes: NetworkManager < %{obsoletes_ifcfg_rh}
+%endif
+
+%if 0%{?rhel} && 0%{?rhel} <= 7
+# Kept for RHEL to ensure that wired 802.1x works out of the box
+Requires: wpa_supplicant >= 1:1.1
+%endif
+
+Conflicts: NetworkManager-vpnc < 1:0.7.0.99-1
+Conflicts: NetworkManager-openvpn < 1:0.7.0.99-1
+Conflicts: NetworkManager-pptp < 1:0.7.0.99-1
+Conflicts: NetworkManager-openconnect < 0:0.7.0.99-1
+Conflicts: kde-plasma-networkmanagement < 1:0.9-0.49.20110527git.nm09
+
+BuildRequires: make
+BuildRequires: gcc
+BuildRequires: libtool
+BuildRequires: pkgconfig
+%if %{with meson}
+BuildRequires: meson
+%else
+BuildRequires: automake
+BuildRequires: autoconf
+%endif
+BuildRequires: gettext-devel >= 0.19.8
+
+BuildRequires: dbus-devel >= %{dbus_version}
+BuildRequires: glib2-devel >= 2.40.0
+BuildRequires: gobject-introspection-devel >= 0.10.3
+%if %{with ppp}
+BuildRequires: ppp-devel >= 2.4.5
+%endif
+%if %{with crypto_gnutls}
+BuildRequires: gnutls-devel >= 2.12
+%else
+BuildRequires: nss-devel >= 3.11.7
+%endif
+BuildRequires: readline-devel
+BuildRequires: audit-libs-devel
+%if %{with regen_docs}
+BuildRequires: gtk-doc
+%endif
+BuildRequires: libudev-devel
+BuildRequires: libuuid-devel
+BuildRequires: /usr/bin/valac
+BuildRequires: libxslt
+%if %{with bluetooth}
+BuildRequires: bluez-libs-devel
+%endif
+BuildRequires: systemd >= 200-3 systemd-devel
+%if 0%{?fedora}
+BuildRequires: libpsl-devel >= 0.1
+%endif
+BuildRequires: libcurl-devel
+BuildRequires: libndp-devel >= 1.0
+%if 0%{?with_modem_manager_1}
+BuildRequires: ModemManager-glib-devel >= 1.0
+%endif
+%if %{with wwan}
+BuildRequires: mobile-broadband-provider-info-devel
+%endif
+%if %{with nmtui}
+BuildRequires: newt-devel
+%endif
+BuildRequires: /usr/bin/dbus-launch
+BuildRequires: python3
+BuildRequires: python3-gobject-base
+BuildRequires: python3-dbus
+BuildRequires: python3-pexpect
+BuildRequires: libselinux-devel
+BuildRequires: polkit-devel
+BuildRequires: jansson-devel
+%if %{with sanitizer}
+BuildRequires: libasan
+%if 0%{?fedora} || 0%{?rhel} >= 8
+BuildRequires: libubsan
+%endif
+%endif
+%if %{with firewalld_zone}
+BuildRequires: firewalld-filesystem
+%endif
+BuildRequires: iproute
+%if 0%{?fedora} || 0%{?rhel} >= 8
+BuildRequires: iproute-tc
+%endif
+
+Provides: %{name}-dispatcher%{?_isa} = %{epoch}:%{version}-%{release}
+
+# NetworkManager uses various parts of systemd-networkd internally, including
+# DHCP client, IPv4 Link-Local address negotiation or LLDP support.
+# This provide is essentially here so that NetworkManager shows on Security
+# Response Team's radar in case a flaw is found. The code is frequently
+# synchronized and thus it's not easy to establish a good version number
+# here. The version of zero is there just to have something conservative so
+# that the scripts that would parse the SPEC file naively would be unlikely
+# to fail. Refer to git log for the real date and commit number of last
+# synchronization:
+# https://gitlab.freedesktop.org/NetworkManager/NetworkManager/commits/main/src/
+Provides: bundled(systemd) = 0
+
+
+%description
+NetworkManager is a system service that manages network interfaces and
+connections based on user or automatic configuration. It supports
+Ethernet, Bridge, Bond, VLAN, Team, InfiniBand, Wi-Fi, mobile broadband
+(WWAN), PPPoE and other devices, and supports a variety of different VPN
+services.
+
+
+%if %{with adsl}
+%package adsl
+Summary: ADSL device plugin for NetworkManager
+Group: System Environment/Base
+Requires: %{name}%{?_isa} = %{epoch}:%{version}-%{release}
+Obsoletes: NetworkManager < %{obsoletes_device_plugins}
+
+%description adsl
+This package contains NetworkManager support for ADSL devices.
+%endif
+
+
+%if %{with bluetooth}
+%package bluetooth
+Summary: Bluetooth device plugin for NetworkManager
+Group: System Environment/Base
+Requires: %{name}%{?_isa} = %{epoch}:%{version}-%{release}
+Requires: NetworkManager-wwan = %{epoch}:%{version}-%{release}
+%if 0%{?rhel} && 0%{?rhel} <= 7
+# No Requires:bluez to prevent it being installed when updating
+# to the split NM package
+%else
+Requires: bluez >= 4.101-5
+%endif
+Obsoletes: NetworkManager < %{obsoletes_device_plugins}
+
+%description bluetooth
+This package contains NetworkManager support for Bluetooth devices.
+%endif
+
+
+%if %{with team}
+%package team
+Summary: Team device plugin for NetworkManager
+Group: System Environment/Base
+BuildRequires: teamd-devel
+Requires: %{name}%{?_isa} = %{epoch}:%{version}-%{release}
+Obsoletes: NetworkManager < %{obsoletes_device_plugins}
+%if 0%{?fedora} || 0%{?rhel} >= 8
+# Team was split from main NM binary between 0.9.10 and 1.0
+# We need this Obsoletes in addition to the one above
+# (git:3aede801521ef7bff039e6e3f1b3c7b566b4338d).
+Obsoletes: NetworkManager < 1:1.0.0
+%endif
+
+%description team
+This package contains NetworkManager support for team devices.
+%endif
+
+
+%if %{with wifi}
+%package wifi
+Summary: Wifi plugin for NetworkManager
+Group: System Environment/Base
+Requires: %{name}%{?_isa} = %{epoch}:%{version}-%{release}
+
+%if 0%{?fedora} >= 29 || 0%{?rhel} >= 9
+Requires: wireless-regdb
+%else
+Requires: crda
+%endif
+
+%if %{with iwd} && (0%{?fedora} >= 25 || 0%{?rhel} >= 8)
+Requires: (wpa_supplicant >= %{wpa_supplicant_version} or iwd)
+Suggests: wpa_supplicant
+%else
+# Just require wpa_supplicant on platforms that don't support boolean
+# dependencies even though the plugin supports both supplicant and
+# iwd backend.
+Requires: wpa_supplicant >= %{wpa_supplicant_version}
+%endif
+
+Obsoletes: NetworkManager < %{obsoletes_device_plugins}
+
+%description wifi
+This package contains NetworkManager support for Wifi and OLPC devices.
+%endif
+
+
+%if %{with wwan}
+%package wwan
+Summary: Mobile broadband device plugin for NetworkManager
+Group: System Environment/Base
+Requires: %{name}%{?_isa} = %{epoch}:%{version}-%{release}
+%if 0%{?rhel} && 0%{?rhel} <= 7
+# No Requires:ModemManager to prevent it being installed when updating
+# to the split NM package
+%else
+Requires: ModemManager
+%endif
+Obsoletes: NetworkManager < %{obsoletes_device_plugins}
+
+%description wwan
+This package contains NetworkManager support for mobile broadband (WWAN)
+devices.
+%endif
+
+
+%if %{with ovs}
+%package ovs
+Summary: Open vSwitch device plugin for NetworkManager
+Group: System Environment/Base
+Requires: %{name}%{?_isa} = %{epoch}:%{version}-%{release}
+%if 0%{?rhel} == 0
+Requires: openvswitch
+%endif
+
+%description ovs
+This package contains NetworkManager support for Open vSwitch bridges.
+%endif
+
+
+%if %{with ppp}
+%package ppp
+Summary: PPP plugin for NetworkManager
+Group: System Environment/Base
+Requires: %{name}%{?_isa} = %{epoch}:%{version}-%{release}
+Requires: ppp = %{ppp_version}
+Requires: NetworkManager = %{epoch}:%{version}-%{release}
+Obsoletes: NetworkManager < %{obsoletes_ppp_plugin}
+
+%description ppp
+This package contains NetworkManager support for PPP.
+%endif
+
+
+%package libnm
+Summary: Libraries for adding NetworkManager support to applications.
+Group: Development/Libraries
+Conflicts: NetworkManager-glib < 1:1.31.0
+License: LGPL-2.1-or-later
+
+%description libnm
+This package contains the libraries that make it easier to use some
+NetworkManager functionality from applications.
+
+
+%package libnm-devel
+Summary: Header files for adding NetworkManager support to applications.
+Group: Development/Libraries
+Requires: %{name}-libnm%{?_isa} = %{epoch}:%{version}-%{release}
+Requires: glib2-devel
+Requires: pkgconfig
+License: LGPL-2.1-or-later
+
+%description libnm-devel
+This package contains the header and pkg-config files for development
+applications using NetworkManager functionality from applications.
+
+
+%if %{with connectivity_fedora}
+%package config-connectivity-fedora
+Summary: NetworkManager config file for connectivity checking via Fedora servers
+Group: System Environment/Base
+BuildArch: noarch
+Provides: NetworkManager-config-connectivity = %{epoch}:%{version}-%{release}
+
+%description config-connectivity-fedora
+This adds a NetworkManager configuration file to enable connectivity checking
+via Fedora infrastructure.
+%endif
+
+
+%if %{with connectivity_redhat}
+%package config-connectivity-redhat
+Summary: NetworkManager config file for connectivity checking via Red Hat servers
+Group: System Environment/Base
+BuildArch: noarch
+Provides: NetworkManager-config-connectivity = %{epoch}:%{version}-%{release}
+
+%description config-connectivity-redhat
+This adds a NetworkManager configuration file to enable connectivity checking
+via Red Hat infrastructure.
+%endif
+
+
+%package config-server
+Summary: NetworkManager config file for "server-like" defaults
+Group: System Environment/Base
+BuildArch: noarch
+
+%description config-server
+This adds a NetworkManager configuration file to make it behave more
+like the old "network" service. In particular, it stops NetworkManager
+from automatically running DHCP on unconfigured ethernet devices, and
+allows connections with static IP addresses to be brought up even on
+ethernet devices with no carrier.
+
+This package is intended to be installed by default for server
+deployments.
+
+
+%if %{?with_ifcfg_rh}
+%package dispatcher-routing-rules
+Summary: NetworkManager dispatcher file for advanced routing rules
+Group: System Environment/Base
+%if 0%{?split_ifcfg_rh}
+Requires: %{name}-initscripts-ifcfg-rh
+%endif
+BuildArch: noarch
+Provides: %{name}-config-routing-rules = %{epoch}:%{version}-%{release}
+Obsoletes: %{name}-config-routing-rules < 1:1.31.0
+
+%description dispatcher-routing-rules
+This adds a NetworkManager dispatcher file to support networking
+configurations using "/etc/sysconfig/network-scripts/rule-NAME" files
+(eg, to do policy-based routing).
+%endif
+
+
+%if %{with nmtui}
+%package tui
+Summary: NetworkManager curses-based UI
+Group: System Environment/Base
+Requires: %{name} = %{epoch}:%{version}-%{release}
+Requires: %{name}-libnm%{?_isa} = %{epoch}:%{version}-%{release}
+
+%description tui
+This adds a curses-based "TUI" (Text User Interface) to
+NetworkManager, to allow performing some of the operations supported
+by nm-connection-editor and nm-applet in a non-graphical environment.
+%endif
+
+
+%if 0%{?split_ifcfg_rh}
+%package initscripts-ifcfg-rh
+Summary: NetworkManager plugin for reading and writing connections in ifcfg-rh format
+Group: System Environment/Base
+Requires: %{name} = %{epoch}:%{version}-%{release}
+Obsoletes: NetworkManager < %{obsoletes_ifcfg_rh}
+
+%description initscripts-ifcfg-rh
+Installs a plugin for reading and writing connection profiles using
+the Red Hat ifcfg format in /etc/sysconfig/network-scripts/.
+%endif
+
+
+%if %{with nm_cloud_setup}
+%package cloud-setup
+Summary: Automatically configure NetworkManager in cloud
+Group: System Environment/Base
+Requires: %{name} = %{epoch}:%{version}-%{release}
+Requires: %{name}-libnm%{?_isa} = %{epoch}:%{version}-%{release}
+
+%description cloud-setup
+Installs a nm-cloud-setup tool that can automatically configure
+NetworkManager in cloud environment. Only certain cloud providers
+like Aliyun, Azure, EC2, GCP are supported.
+%endif
+
+
+%if %{?with_ifcfg_rh}
+%package initscripts-updown
+Summary: Legacy ifup/ifdown scripts for NetworkManager that replace initscripts (network-scripts)
+Group: System Environment/Base
+BuildArch: noarch
+Requires: NetworkManager
+Requires: /usr/bin/nmcli
+Obsoletes: NetworkManager < %{obsoletes_initscripts_updown}
+
+%description initscripts-updown
+Installs alternative ifup/ifdown scripts that talk to NetworkManager.
+This is only for backward compatibility with initscripts (network-scripts).
+Preferably use nmcli instead.
+%endif
+
+%prep
+%autosetup -p1 -n NetworkManager-%{real_version}
+
+
+%build
+%if %{with meson}
+%meson \
+	-Db_ndebug=false \
+	--warnlevel 2 \
+%if %{with test}
+	--werror \
+%endif
+	-Dnft=%{_sbindir}/nft \
+	-Diptables=%{_sbindir}/iptables \
+	-Ddhclient=%{_sbindir}/dhclient \
+	-Ddhcpcanon=no \
+	-Ddhcpcd=no \
+	-Dconfig_dhcp_default=%{dhcp_default} \
+%if %{with crypto_gnutls}
+	-Dcrypto=gnutls \
+%else
+	-Dcrypto=nss \
+%endif
+%if %{with debug}
+	-Dmore_logging=true \
+	-Dmore_asserts=10000 \
+%else
+	-Dmore_logging=false \
+	-Dmore_asserts=0 \
+%endif
+	-Dld_gc=true \
+%if %{with lto}
+	-D b_lto=true \
+%else
+	-D b_lto=false \
+%endif
+	-Dlibaudit=yes-disabled-by-default \
+%if 0%{?with_modem_manager_1}
+	-Dmodem_manager=true \
+%else
+	-Dmodem_manager=false \
+%endif
+%if %{with wifi}
+	-Dwifi=true \
+%if 0%{?fedora}
+	-Dwext=true \
+%else
+	-Dwext=false \
+%endif
+%else
+	-Dwifi=false \
+%endif
+%if %{with iwd}
+	-Diwd=true \
+%else
+	-Diwd=false \
+%endif
+%if %{with bluetooth}
+	-Dbluez5_dun=true \
+%else
+	-Dbluez5_dun=false \
+%endif
+%if %{with nmtui}
+	-Dnmtui=true \
+%else
+	-Dnmtui=false \
+%endif
+%if %{with nm_cloud_setup}
+	-Dnm_cloud_setup=true \
+%else
+	-Dnm_cloud_setup=false \
+%endif
+	-Dvapi=true \
+	-Dintrospection=true \
+%if %{with regen_docs}
+	-Ddocs=true \
+%else
+	-Ddocs=false \
+%endif
+	-Dqt=false \
+%if %{with team}
+	-Dteamdctl=true \
+%else
+	-Dteamdctl=false \
+%endif
+%if %{with ovs}
+	-Dovs=true \
+%else
+	-Dovs=false \
+%endif
+	-Dselinux=true \
+	-Dpolkit=true  \
+	-Dconfig_auth_polkit_default=true \
+	-Dmodify_system=true \
+	-Dconcheck=true \
+%if 0%{?fedora}
+	-Dlibpsl=true \
+%else
+	-Dlibpsl=false \
+%endif
+%if %{ebpf_enabled} != "yes"
+	-Debpf=false \
+%else
+	-Debpf=true \
+%endif
+	-Dsession_tracking=systemd \
+	-Dsuspend_resume=systemd \
+	-Dsystemdsystemunitdir=%{_unitdir} \
+	-Dsystem_ca_path=/etc/pki/tls/cert.pem \
+	-Ddbus_conf_dir=%{dbus_sys_dir} \
+	-Dtests=yes \
+	-Dvalgrind=no \
+%if %{?with_ifcfg_rh}
+	-Difcfg_rh=true \
+%else
+	-Difcfg_rh=false \
+%endif
+	-Difupdown=false \
+%if %{with ppp}
+	-Dppp=true \
+	-Dpppd="%{_sbindir}/pppd" \
+	-Dpppd_plugin_dir="%{_libdir}/pppd/%{ppp_version}" \
+%else
+	-Dppp=false \
+%endif
+%if %{with firewalld_zone}
+	-Dfirewalld_zone=true \
+%else
+	-Dfirewalld_zone=false \
+%endif
+	-Ddist_version=%{version}-%{release} \
+%if %{?config_plugins_default_ifcfg_rh}
+	-Dconfig_plugins_default=ifcfg-rh \
+%endif
+%if %{?ifcfg_migrate}
+	-Dconfig_migrate_ifcfg_rh_default=true \
+%endif
+	-Dresolvconf=no \
+	-Dnetconfig=no \
+	-Dconfig_dns_rc_manager_default=%{dns_rc_manager_default} \
+	-Dconfig_logging_backend_default=%{logging_backend_default}
+
+%meson_build
+
+%else
+# autotools
+%if %{with regen_docs}
+gtkdocize
+%endif
+autoreconf --install --force
+%configure \
+	--with-runstatedir=%{_rundir} \
+	--enable-silent-rules=no \
+	--enable-static=no \
+	--with-nft=%{_sbindir}/nft \
+	--with-iptables=%{_sbindir}/iptables \
+	--with-dhclient=%{_sbindir}/dhclient \
+	--with-dhcpcd=no \
+	--with-dhcpcanon=no \
+	--with-config-dhcp-default=%{dhcp_default} \
+%if %{with crypto_gnutls}
+	--with-crypto=gnutls \
+%else
+	--with-crypto=nss \
+%endif
+%if %{with sanitizer}
+	--with-address-sanitizer=exec \
+%if 0%{?fedora} || 0%{?rhel} >= 8
+	--enable-undefined-sanitizer=yes \
+%else
+	--enable-undefined-sanitizer=no \
+%endif
+%else
+	--with-address-sanitizer=no \
+	--enable-undefined-sanitizer=no \
+%endif
+%if %{with debug}
+	--enable-more-logging=yes \
+	--with-more-asserts=10000 \
+%else
+	--enable-more-logging=no \
+	--with-more-asserts=0 \
+%endif
+	--enable-ld-gc=yes \
+%if %{with lto}
+	--enable-lto=yes \
+%else
+	--enable-lto=no \
+%endif
+	--with-libaudit=yes-disabled-by-default \
+%if 0%{?with_modem_manager_1}
+	--with-modem-manager-1=yes \
+%else
+	--with-modem-manager-1=no \
+%endif
+%if %{with wifi}
+	--enable-wifi=yes \
+%if 0%{?fedora}
+	--with-wext=yes \
+%else
+	--with-wext=no \
+%endif
+%else
+	--enable-wifi=no \
+%endif
+%if %{with iwd}
+	--with-iwd=yes \
+%else
+	--with-iwd=no \
+%endif
+%if %{with bluetooth}
+	--enable-bluez5-dun=yes \
+%else
+	--enable-bluez5-dun=no \
+%endif
+%if %{with nmtui}
+	--with-nmtui=yes \
+%else
+	--with-nmtui=no \
+%endif
+%if %{with nm_cloud_setup}
+	--with-nm-cloud-setup=yes \
+%else
+	--with-nm-cloud-setup=no \
+%endif
+	--enable-vala=yes \
+	--enable-introspection=yes \
+%if %{with regen_docs}
+	--enable-gtk-doc=yes \
+%else
+	--enable-gtk-doc=no \
+%endif
+%if %{with team}
+	--enable-teamdctl=yes \
+%else
+	--enable-teamdctl=no \
+%endif
+%if %{with ovs}
+	--enable-ovs=yes \
+%else
+	--enable-ovs=no \
+%endif
+	--with-selinux=yes \
+	--enable-polkit=yes \
+	--enable-modify-system=yes \
+	--enable-concheck=yes \
+%if 0%{?fedora}
+	--with-libpsl=yes \
+%else
+	--with-libpsl=no \
+%endif
+	--with-ebpf=%{ebpf_enabled} \
+	--with-session-tracking=systemd \
+	--with-suspend-resume=systemd \
+	--with-systemdsystemunitdir=%{_unitdir} \
+	--with-system-ca-path=/etc/pki/tls/cert.pem \
+	--with-dbus-sys-dir=%{dbus_sys_dir} \
+	--with-tests=yes \
+%if %{with test}
+	--enable-more-warnings=error \
+%else
+	--enable-more-warnings=yes \
+%endif
+	--with-valgrind=no \
+%if %{?with_ifcfg_rh}
+	--enable-ifcfg-rh=yes \
+%else
+        --enable-ifcfg-rh=no \
+%endif
+	--enable-ifupdown=no \
+%if %{with ppp}
+	--enable-ppp=yes \
+	--with-pppd="%{_sbindir}/pppd" \
+	--with-pppd-plugin-dir="%{_libdir}/pppd/%{ppp_version}" \
+%else
+	--enable-ppp=no \
+%endif
+%if %{with firewalld_zone}
+	--enable-firewalld-zone=yes \
+%else
+	--enable-firewalld-zone=no \
+%endif
+	--with-dist-version=%{version}-%{release} \
+%if %{?config_plugins_default_ifcfg_rh}
+	--with-config-plugins-default=ifcfg-rh \
+%endif
+%if %{?ifcfg_migrate}
+	--with-config-migrate-ifcfg-rh-default=yes \
+%endif
+	--with-resolvconf=no \
+	--with-netconfig=no \
+	--with-config-dns-rc-manager-default=%{dns_rc_manager_default} \
+	--with-config-logging-backend-default=%{logging_backend_default}
+
+%make_build
+
+%endif
+
+%install
+%if %{with meson}
+%meson_install
+%else
+%make_install
+%endif
+
+cp %{SOURCE1} %{buildroot}%{_sysconfdir}/%{name}/
+
+cp %{SOURCE2} %{buildroot}%{nmlibdir}/conf.d/
+
+%if %{with connectivity_fedora}
+cp %{SOURCE4} %{buildroot}%{nmlibdir}/conf.d/
+%endif
+
+%if %{with connectivity_redhat}
+cp %{SOURCE5} %{buildroot}%{nmlibdir}/conf.d/
+mkdir -p %{buildroot}%{_sysctldir}
+cp %{SOURCE7} %{buildroot}%{_sysctldir}
+%endif
+
+%if 0%{?fedora} >= 40
+cp %{SOURCE6} %{buildroot}%{nmlibdir}/conf.d/
+%endif
+
+%if 0%{?ifcfg_warning}
+cp %{SOURCE8} %{buildroot}%{_sysconfdir}/sysconfig/network-scripts
+%endif
+%if 0%{?ifcfg_migrate}
+cp %{SOURCE9} %{buildroot}%{_sysconfdir}/sysconfig/network-scripts/readme-ifcfg-rh.txt
+%endif
+
+%if %{?with_ifcfg_rh}
+cp examples/dispatcher/10-ifcfg-rh-routes.sh %{buildroot}%{nmlibdir}/dispatcher.d/
+ln -s ../no-wait.d/10-ifcfg-rh-routes.sh %{buildroot}%{nmlibdir}/dispatcher.d/pre-up.d/
+ln -s ../10-ifcfg-rh-routes.sh %{buildroot}%{nmlibdir}/dispatcher.d/no-wait.d/
+%endif
+
+%find_lang %{name}
+
+rm -f %{buildroot}%{_libdir}/*.la
+rm -f %{buildroot}%{_libdir}/pppd/%{ppp_version}/*.la
+rm -f %{buildroot}%{nmplugindir}/*.la
+
+# Ensure the documentation timestamps are constant to avoid multilib conflicts
+find %{buildroot}%{_datadir}/gtk-doc -exec touch --reference configure.ac '{}' \+
+
+%if 0%{?__debug_package} && ! 0%{?flatpak}
+mkdir -p %{buildroot}%{_prefix}/src/debug/NetworkManager-%{real_version}
+cp valgrind.suppressions %{buildroot}%{_prefix}/src/debug/NetworkManager-%{real_version}
+%endif
+
+%if %{?with_ifcfg_rh}
+touch %{buildroot}%{_sbindir}/ifup
+touch %{buildroot}%{_sbindir}/ifdown
+%endif
+
+%check
+%if %{with meson}
+%if %{with test}
+%meson_test
+%else
+%ninja_test -C %{_vpath_builddir} || :
+%endif
+%else
+# autotools
+%if %{with test}
+make -k %{?_smp_mflags} check
+%else
+make -k %{?_smp_mflags} check || :
+%endif
+%endif
+
+
+%pre
+if [ -f "%{_unitdir}/network-online.target.wants/NetworkManager-wait-online.service" ] ; then
+    # older versions used to install this file, effectively always enabling
+    # NetworkManager-wait-online.service. We no longer do that and rely on
+    # preset.
+    # But on package upgrade we must explicitly enable it (rh#1455704).
+    systemctl enable NetworkManager-wait-online.service || :
+fi
+
+
+%post
+# skip triggering if udevd isn't even accessible, e.g. containers or
+# rpm-ostree-based systems
+if [ -S /run/udev/control ]; then
+    /usr/bin/udevadm control --reload-rules || :
+    /usr/bin/udevadm trigger --subsystem-match=net || :
+fi
+%if %{with firewalld_zone}
+%firewalld_reload
+%endif
+
+%systemd_post %{systemd_units}
+
+
+%if %{?with_ifcfg_rh}
+%post initscripts-updown
+if [ -f %{_sbindir}/ifup -a ! -L %{_sbindir}/ifup ]; then
+    # initscripts package too old, won't let us set an alternative
+    /usr/sbin/update-alternatives --remove ifup %{_libexecdir}/nm-ifup >/dev/null 2>&1 || :
+else
+    /usr/sbin/update-alternatives --install %{_sbindir}/ifup ifup %{_libexecdir}/nm-ifup 50 \
+        --slave %{_sbindir}/ifdown ifdown %{_libexecdir}/nm-ifdown
+fi
+%endif
+
+
+%if %{with nm_cloud_setup}
+%post cloud-setup
+%systemd_post %{systemd_units_cloud_setup}
+%endif
+
+
+%preun
+if [ $1 -eq 0 ]; then
+    # Package removal, not upgrade
+    /bin/systemctl --no-reload disable NetworkManager.service >/dev/null 2>&1 || :
+
+    # Don't kill networking entirely just on package remove
+    #/bin/systemctl stop NetworkManager.service >/dev/null 2>&1 || :
+fi
+%systemd_preun NetworkManager-wait-online.service NetworkManager-dispatcher.service nm-priv-helper.service
+
+
+%if %{?with_ifcfg_rh}
+%preun initscripts-updown
+if [ $1 -eq 0 ]; then
+    /usr/sbin/update-alternatives --remove ifup %{_libexecdir}/nm-ifup >/dev/null 2>&1 || :
+fi
+%endif
+
+
+%if %{with nm_cloud_setup}
+%preun cloud-setup
+%systemd_preun %{systemd_units_cloud_setup}
+%endif
+
+
+%postun
+/usr/bin/udevadm control --reload-rules || :
+/usr/bin/udevadm trigger --subsystem-match=net || :
+%if %{with firewalld_zone}
+%firewalld_reload
+%endif
+
+%systemd_postun %{systemd_units}
+
+
+%if (0%{?fedora} && 0%{?fedora} < 28) || 0%{?rhel}
+%post   libnm -p /sbin/ldconfig
+%postun libnm -p /sbin/ldconfig
+%endif
+
+
+%if %{with nm_cloud_setup}
+%postun cloud-setup
+%systemd_postun %{systemd_units_cloud_setup}
+%endif
+
+
+%files
+%{dbus_sys_dir}/org.freedesktop.NetworkManager.conf
+%{dbus_sys_dir}/nm-dispatcher.conf
+%{dbus_sys_dir}/nm-priv-helper.conf
+%if %{?with_ifcfg_rh} && 0%{?split_ifcfg_rh} == 0
+%{dbus_sys_dir}/nm-ifcfg-rh.conf
+%endif
+%{_sbindir}/%{name}
+%{_bindir}/nmcli
+%{_datadir}/bash-completion/completions/nmcli
+%dir %{_sysconfdir}/%{name}
+%dir %{_sysconfdir}/%{name}/conf.d
+%dir %{_sysconfdir}/%{name}/dispatcher.d
+%dir %{_sysconfdir}/%{name}/dispatcher.d/pre-down.d
+%dir %{_sysconfdir}/%{name}/dispatcher.d/pre-up.d
+%dir %{_sysconfdir}/%{name}/dispatcher.d/no-wait.d
+%dir %{_sysconfdir}/%{name}/dnsmasq.d
+%dir %{_sysconfdir}/%{name}/dnsmasq-shared.d
+%dir %{_sysconfdir}/%{name}/system-connections
+%config(noreplace) %{_sysconfdir}/%{name}/NetworkManager.conf
+%if 0%{?fedora} >= 40
+%{nmlibdir}/conf.d/22-wifi-mac-addr.conf
+%endif
+%ghost %{_sysconfdir}/%{name}/VPN
+%{_bindir}/nm-online
+%{_libexecdir}/nm-dhcp-helper
+%{_libexecdir}/nm-dispatcher
+%{_libexecdir}/nm-initrd-generator
+%{_libexecdir}/nm-daemon-helper
+%{_libexecdir}/nm-priv-helper
+%dir %{_libdir}/%{name}
+%dir %{nmplugindir}
+%if %{?with_ifcfg_rh} && 0%{?split_ifcfg_rh} == 0
+%{nmplugindir}/libnm-settings-plugin-ifcfg-rh.so
+%endif
+%if %{with nmtui}
+%exclude %{_mandir}/man1/nmtui*
+%endif
+%dir %{nmlibdir}
+%dir %{nmlibdir}/conf.d
+%dir %{nmlibdir}/dispatcher.d
+%dir %{nmlibdir}/dispatcher.d/pre-down.d
+%dir %{nmlibdir}/dispatcher.d/pre-up.d
+%dir %{nmlibdir}/dispatcher.d/no-wait.d
+%dir %{nmlibdir}/VPN
+%dir %{nmlibdir}/system-connections
+%{_mandir}/man1/*
+%{_mandir}/man5/*
+%{_mandir}/man7/nmcli-examples.7*
+%{_mandir}/man8/nm-initrd-generator.8*
+%{_mandir}/man8/NetworkManager.8*
+%{_mandir}/man8/NetworkManager-dispatcher.8*
+%{_mandir}/man8/NetworkManager-wait-online.service.8*
+%dir %{_localstatedir}/lib/NetworkManager
+%if %{?with_ifcfg_rh}
+%dir %{_sysconfdir}/sysconfig/network-scripts
+%endif
+%{_datadir}/dbus-1/system-services/org.freedesktop.nm_dispatcher.service
+%{_datadir}/dbus-1/system-services/org.freedesktop.nm_priv_helper.service
+%{_datadir}/polkit-1/actions/*.policy
+%{_prefix}/lib/udev/rules.d/*.rules
+%if %{with firewalld_zone}
+%{_prefix}/lib/firewalld/zones/nm-shared.xml
+%endif
+# systemd stuff
+%{_unitdir}/NetworkManager.service
+%{_unitdir}/NetworkManager-wait-online.service
+%{_unitdir}/NetworkManager-dispatcher.service
+%{_unitdir}/nm-priv-helper.service
+%dir %{_datadir}/doc/NetworkManager/examples
+%{_datadir}/doc/NetworkManager/examples/server.conf
+%if 0%{?ifcfg_warning} || 0%{?ifcfg_migrate}
+%{_sysconfdir}/sysconfig/network-scripts/readme-ifcfg-rh.txt
+%endif
+%doc NEWS AUTHORS README.md CONTRIBUTING.md
+%license COPYING
+%license COPYING.LGPL
+%license COPYING.GFDL
+
+
+%if %{with adsl}
+%files adsl
+%{nmplugindir}/libnm-device-plugin-adsl.so
+%else
+%exclude %{nmplugindir}/libnm-device-plugin-adsl.so
+%endif
+
+
+%if %{with bluetooth}
+%files bluetooth
+%{nmplugindir}/libnm-device-plugin-bluetooth.so
+%endif
+
+
+%if %{with team}
+%files team
+%{nmplugindir}/libnm-device-plugin-team.so
+%endif
+
+
+%if %{with wifi}
+%files wifi
+%{nmplugindir}/libnm-device-plugin-wifi.so
+%endif
+
+
+%if %{with wwan}
+%files wwan
+%{nmplugindir}/libnm-device-plugin-wwan.so
+%{nmplugindir}/libnm-wwan.so
+%endif
+
+
+%if %{with ovs}
+%files ovs
+%{nmplugindir}/libnm-device-plugin-ovs.so
+%{_unitdir}/NetworkManager.service.d/NetworkManager-ovs.conf
+%{_mandir}/man7/nm-openvswitch.7*
+%endif
+
+
+%if %{with ppp}
+%files ppp
+%{_libdir}/pppd/%{ppp_version}/nm-pppd-plugin.so
+%{nmplugindir}/libnm-ppp-plugin.so
+%endif
+
+
+%files libnm -f %{name}.lang
+%{_libdir}/libnm.so.*
+%{_libdir}/girepository-1.0/NM-1.0.typelib
+
+
+%files libnm-devel
+%dir %{_includedir}/libnm
+%{_includedir}/libnm/*.h
+%{_libdir}/pkgconfig/libnm.pc
+%{_libdir}/libnm.so
+%{_datadir}/gir-1.0/NM-1.0.gir
+%dir %{_datadir}/gtk-doc/html/libnm
+%{_datadir}/gtk-doc/html/libnm/*
+%dir %{_datadir}/gtk-doc/html/NetworkManager
+%{_datadir}/gtk-doc/html/NetworkManager/*
+%{_datadir}/vala/vapi/libnm.deps
+%{_datadir}/vala/vapi/libnm.vapi
+%{_datadir}/dbus-1/interfaces/*.xml
+
+
+%if %{with connectivity_fedora}
+%files config-connectivity-fedora
+%dir %{nmlibdir}
+%dir %{nmlibdir}/conf.d
+%{nmlibdir}/conf.d/20-connectivity-fedora.conf
+%endif
+
+
+%if %{with connectivity_redhat}
+%files config-connectivity-redhat
+%dir %{nmlibdir}
+%dir %{nmlibdir}/conf.d
+%{nmlibdir}/conf.d/20-connectivity-redhat.conf
+%{_sysctldir}/70-nm-connectivity.conf
+%endif
+
+
+%files config-server
+%dir %{nmlibdir}
+%dir %{nmlibdir}/conf.d
+%{nmlibdir}/conf.d/00-server.conf
+
+
+%if %{?with_ifcfg_rh}
+%files dispatcher-routing-rules
+%{nmlibdir}/dispatcher.d/10-ifcfg-rh-routes.sh
+%{nmlibdir}/dispatcher.d/no-wait.d/10-ifcfg-rh-routes.sh
+%{nmlibdir}/dispatcher.d/pre-up.d/10-ifcfg-rh-routes.sh
+%endif
+
+%if %{with nmtui}
+%files tui
+%{_bindir}/nmtui
+%{_bindir}/nmtui-edit
+%{_bindir}/nmtui-connect
+%{_bindir}/nmtui-hostname
+%{_mandir}/man1/nmtui*
+%endif
+
+
+%if 0%{?split_ifcfg_rh}
+%files initscripts-ifcfg-rh
+%{nmplugindir}/libnm-settings-plugin-ifcfg-rh.so
+%{dbus_sys_dir}/nm-ifcfg-rh.conf
+%endif
+
+
+%if %{with nm_cloud_setup}
+%files cloud-setup
+%{_libexecdir}/nm-cloud-setup
+%{_unitdir}/nm-cloud-setup.service
+%{_unitdir}/nm-cloud-setup.timer
+%{nmlibdir}/dispatcher.d/90-nm-cloud-setup.sh
+%{nmlibdir}/dispatcher.d/no-wait.d/90-nm-cloud-setup.sh
+%{nmlibdir}/dispatcher.d/pre-up.d/90-nm-cloud-setup.sh
+%{_mandir}/man8/nm-cloud-setup.8*
+%endif
+
+
+%if %{?with_ifcfg_rh}
+%files initscripts-updown
+%{_libexecdir}/nm-ifup
+%ghost %attr(755, root, root) %{_sbindir}/ifup
+%{_libexecdir}/nm-ifdown
+%ghost %attr(755, root, root) %{_sbindir}/ifdown
+%endif
+
+
+%changelog
+__CHANGELOG__
diff --git a/contrib/fedora/rpm/README b/contrib/fedora/rpm/README
new file mode 100644
index 00000000..7982a1ef
--- /dev/null
+++ b/contrib/fedora/rpm/README
@@ -0,0 +1,27 @@
+# To build RPM packages for Fedora derivates directly from git, just do:
+
+
+#
+# preparation:
+#
+git clone https://gitlab.freedesktop.org/NetworkManager/NetworkManager.git
+cd NetworkManager
+git checkout $WHATEVER
+./contrib/fedora/REQUIRED_PACKAGES
+
+
+#
+# build the packages. Pass --help for usage help.
+#
+./contrib/fedora/rpm/build_clean.sh
+
+
+#
+# install
+#
+sudo dnf install ./contrib/fedora/rpm/latest/RPMS/x86_64/*rpm
+
+
+
+# To generate a clean build from git using mock, run:
+./contrib/fedora/rpm/mockbuild.sh
diff --git a/contrib/fedora/rpm/build.sh b/contrib/fedora/rpm/build.sh
new file mode 100755
index 00000000..8160b915
--- /dev/null
+++ b/contrib/fedora/rpm/build.sh
@@ -0,0 +1,288 @@
+#!/bin/bash
+
+#set -vx
+
+# Set arguments via environment variables.
+# Argument can be omitted and defaults will be detected.
+#
+#   BUILDTYPE=|SRPM
+#   NM_RPMBUILD_ARGS=<additional argus for rpmbuild>
+#   RELEASE_VERSION=
+#   SNAPSHOT=
+#   VERSION=
+#   COMMIT_FULL=
+#   COMMIT=
+#   USERNAME=
+#   SPECFILE=
+#   SOURCE=<path>
+#   SOURCE_FROM_GIT=|1|0
+#   SOURCE_NETWORKMANAGER_CONF=
+#   SOURCE_CONFIG_SERVER=
+#   SOURCE_CONFIG_CONNECTIVITY_FEDORA=
+#   SOURCE_CONFIG_CONNECTIVITY_REDHAT=
+#   SOURCE_SYSCTL_RP_FILTER_REDHAT=
+#   SOURCE_README_IFCFG_FILES=
+#   SOURCE_README_IFCFG_MIGRATED=
+#   SIGN_SOURCE=
+#   DO_RELEASE=
+#   BCOND_DEFAULT_DEBUG=
+#   BCOND_DEFAULT_LTO=
+#   BCOND_DEFAULT_TEST=
+
+die() {
+    echo "$*" >&2
+    exit 1
+}
+
+# copy output also to logfile
+LOG() {
+    echo "$*"
+}
+
+coerce_bool() {
+    case "$1" in
+        no|n|NO|N|0)
+            echo 0
+            ;;
+        yes|y|YES|Y|1)
+            echo 1
+            ;;
+        "")
+            printf '%s' "$2"
+            ;;
+    esac
+}
+
+in_set() {
+    local v="$1"
+    shift
+    for v2; do
+        test "$v" = "$v2" && return 0
+    done
+    return 1
+}
+
+abs_path() {
+    local F="$1"
+
+    if [[ "$F" != "" ]]; then
+        F="$(cd "$ORIGDIR" && readlink -f "$F")" || exit 55
+        [[ -f "$F" ]] || exit 55
+    else
+        F="$2"
+    fi
+    printf '%s' "$F"
+    exit 0
+}
+
+get_version() {
+    local major minor micro
+    local F="${1:-"$GITDIR/configure.ac"}"
+
+    vars="$(sed -n 's/^m4_define(\[nm_\(major\|minor\|micro\)_version\], *\[\([0-9]\+\)\]) *$/local \1='\''\2'\''/p' "$F" 2>/dev/null)"
+    eval "$vars"
+
+    [[ -n "$major" && -n "$minor" && "$micro" ]] || return 1
+    echo "$major.$minor.$micro"
+}
+
+write_changelog() {
+    if [[ "x$CHANGELOG" == x ]]; then
+        cat <<- EOF
+	* $(LC_TIME=C date '+%a %b %d %Y') $USERNAME - %{epoch_version}:%{version}-%{release_version}%{?snap}
+	- build of NetworkManager ($DATE, uuid: $UUID, git: $COMMIT_FULL)
+	$(git log -n20 --date=local --format='- %h %s [%an] (%ci)')
+	- ...
+	EOF
+    else
+        echo "$CHANGELOG"
+    fi > "$TEMP/SOURCES/CHANGELOG"
+}
+
+ORIGDIR="$(readlink -f "$PWD")"
+SCRIPTDIR="$(dirname "$(readlink -f "$0")")"
+LOG "Change to directory \"$SCRIPTDIR\""
+cd "$SCRIPTDIR" || die "could not change into $SCRIPTDIR"
+GITDIR="$(cd "$SCRIPTDIR" && git rev-parse --show-toplevel || die "Could not get GITDIR")"
+
+DATE="$(date '+%Y%m%d-%H%M%S')"
+
+BUILDLOG="$(mktemp ./.build.log.XXXXXXX)"
+chmod +r "$BUILDLOG"
+
+exec > >(tee "$BUILDLOG")
+exec 2>&1
+
+UUID=`uuidgen`
+RELEASE_VERSION="${RELEASE_VERSION:-$(git rev-list HEAD | wc -l)}"
+SNAPSHOT="${SNAPSHOT:-%{nil\}}"
+VERSION="${VERSION:-$(get_version || die "Could not read $VERSION")}"
+COMMIT_FULL="${COMMIT_FULL:-$(git rev-parse --verify HEAD || die "Error reading HEAD revision")}"
+COMMIT="${COMMIT:-$(printf '%s' "$COMMIT_FULL" | sed 's/^\(.\{10\}\).*/\1/' || die "Error reading HEAD revision")}"
+BCOND_DEFAULT_DEBUG="${BCOND_DEFAULT_DEBUG:-0}"
+BCOND_DEFAULT_TEST="${BCOND_DEFAULT_TEST:-0}"
+BCOND_DEFAULT_LTO="${BCOND_DEFAULT_LTO}"
+USERNAME="${USERNAME:-"$(git config user.name) <$(git config user.email)>"}"
+SPECFILE="$(abs_path "$SPECFILE" "$SCRIPTDIR/NetworkManager.spec")" || die "invalid \$SPECFILE argument"
+SOURCE_FROM_GIT="$(coerce_bool "$SOURCE_FROM_GIT" "")"
+SOURCE="$(abs_path "$SOURCE")" || die "invalid \$SOURCE argument"
+DO_RELEASE="$(coerce_bool "$DO_RELEASE" "0")"
+SIGN_SOURCE="$(coerce_bool "$SIGN_SOURCE" "$DO_RELEASE")"
+if [ -n "$SOURCE" ]; then
+    [[ "$SOURCE_FROM_GIT" == 1 ]] && die "Cannot set both \$SOURCE and \$SOURCE_FROM_GIT=1"
+    SOURCE_FROM_GIT=0
+elif [[ "$SOURCE_FROM_GIT" != "1" ]]; then
+    SOURCE="$(ls -1 "$GITDIR/NetworkManager-${VERSION}.tar."* 2>/dev/null | head -n1)"
+    if [[ -z "$SOURCE" ]]; then
+        [[ "$SOURCE_FROM_GIT" == "0" ]] && die "Either set \$SOURCE or set \$SOURCE_FROM_GIT=1"
+        SOURCE_FROM_GIT=1
+    else
+        SOURCE_FROM_GIT=0
+    fi
+fi
+
+SOURCE_NETWORKMANAGER_CONF="$(abs_path "$SOURCE_NETWORKMANAGER_CONF" "$SCRIPTDIR/NetworkManager.conf")" || die "invalid \$SOURCE_NETWORKMANAGER_CONF argument"
+SOURCE_CONFIG_SERVER="$(abs_path "$SOURCE_CONFIG_SERVER" "$SCRIPTDIR/00-server.conf")" || die "invalid \$SOURCE_CONFIG_SERVER argument"
+SOURCE_CONFIG_CONNECTIVITY_FEDORA="$(abs_path "$SOURCE_CONFIG_CONNECTIVITY_FEDORA" "$SCRIPTDIR/20-connectivity-fedora.conf")" || die "invalid \$SOURCE_CONFIG_CONNECTIVITY_FEDORA argument"
+SOURCE_CONFIG_CONNECTIVITY_REDHAT="$(abs_path "$SOURCE_CONFIG_CONNECTIVITY_REDHAT" "$SCRIPTDIR/20-connectivity-redhat.conf")" || die "invalid \$SOURCE_CONFIG_CONNECTIVITY_REDHAT argument"
+SOURCE_CONFIG_WIFI_MAC_ADDR="$(abs_path "$SOURCE_CONFIG_WIFI_MAC_ADDR" "$SCRIPTDIR/22-wifi-mac-addr.conf")" || die "invalid \$SOURCE_CONFIG_WIFI_MAC_ADDR argument"
+SOURCE_SYSCTL_RP_FILTER_REDHAT="$(abs_path "$SOURCE_SYSCTL_RP_FILTER_REDHAT" "$SCRIPTDIR/70-nm-connectivity.conf")" || die "invalid \$SOURCE_SYSCTL_RP_FILTER_REDHAT argument"
+SOURCE_README_IFCFG_FILES="$(abs_path "$SOURCE_README_IFCFG_FILES" "$SCRIPTDIR/readme-ifcfg-rh.txt")" || die "invalid \$SOURCE_README_IFCFG_FILES argument"
+SOURCE_README_IFCFG_MIGRATED="$(abs_path "$SOURCE_README_IFCFG_MIGRATED" "$SCRIPTDIR/readme-ifcfg-rh-migrated.txt")" || die "invalid \$SOURCE_README_IFCFG_MIGRATED argument"
+
+TEMP="$(mktemp -d "$SCRIPTDIR/NetworkManager.$DATE.XXXXXX")"
+TEMPBASE="$(basename "$TEMP")"
+
+if [[ "$SOURCE_FROM_GIT" == "1" ]]; then
+    mkdir -p "$TEMP/SOURCES"
+    SOURCE="$TEMP/SOURCES/NetworkManager-${VERSION}.tar.xz"
+    (cd "$GITDIR" && git archive --prefix="NetworkManager-$VERSION"/ "$COMMIT_FULL") | xz > "$SOURCE"
+fi
+
+LOG "VERSION=$VERSION"
+LOG "RELEASE_VERSION=$RELEASE_VERSION"
+LOG "SNAPSHOT=$SNAPSHOT"
+LOG "COMMIT_FULL=$COMMIT_FULL"
+LOG "COMMIT=$COMMIT"
+LOG "USERNAME=$USERNAME"
+LOG "SPECFILE=$SPECFILE"
+LOG "SOURCE=$SOURCE"
+LOG "SIGN_SOURCE=$SIGN_SOURCE"
+LOG "DO_RELEASE=$DO_RELEASE"
+LOG "SOURCE_FROM_GIT=$SOURCE_FROM_GIT"
+LOG "SOURCE_NETWORKMANAGER_CONF=$SOURCE_NETWORKMANAGER_CONF"
+LOG "SOURCE_CONFIG_SERVER=$SOURCE_CONFIG_SERVER"
+LOG "SOURCE_CONFIG_CONNECTIVITY_FEDORA=$SOURCE_CONFIG_CONNECTIVITY_FEDORA"
+LOG "SOURCE_CONFIG_CONNECTIVITY_REDHAT=$SOURCE_CONFIG_CONNECTIVITY_REDHAT"
+LOG "SOURCE_SYSCTL_RP_FILTER_REDHAT=$SOURCE_SYSCTL_RP_FILTER_REDHAT"
+LOG "SOURCE_README_IFCFG_FILES=$SOURCE_README_IFCFG_FILES"
+LOG "SOURCE_README_IFCFG_MIGRATED=$SOURCE_README_IFCFG_MIGRATED"
+LOG "BUILDTYPE=$BUILDTYPE"
+LOG "NM_RPMBUILD_ARGS=$NM_RPMBUILD_ARGS"
+LOG "BCOND_DEFAULT_DEBUG=$BCOND_DEFAULT_DEBUG"
+LOG "BCOND_DEFAULT_LTO=$BCOND_DEFAULT_LTO"
+LOG "BCOND_DEFAULT_TEST=$BCOND_DEFAULT_TEST"
+LOG ""
+LOG "UUID=$UUID"
+LOG "BASEDIR=$TEMP"
+
+in_set "$BCOND_DEFAULT_DEBUG" 0 1 || die "Invalid value for \$BCOND_DEFAULT_DEBUG: \"$BCOND_DEFAULT_DEBUG\""
+in_set "$BCOND_DEFAULT_LTO" '' 0 1 || die "Invalid value for \$BCOND_DEFAULT_LTO: \"$BCOND_DEFAULT_LTO\""
+in_set "$BCOND_DEFAULT_TEST" 0 1 || die "Invalid value for \$BCOND_DEFAULT_TEST: \"$BCOND_DEFAULT_TEST\""
+
+ln -snf "$TEMPBASE" ./latest0
+ln "$BUILDLOG" "$TEMPBASE/build.log"
+rm -f "$BUILDLOG"
+
+TEMPSPEC="$TEMP/SPECS/NetworkManager.spec"
+mkdir -p "$TEMP/SOURCES/" "$TEMP/SPECS/" || die "error creating SPECS directory"
+
+if [[ "$(dirname "$SOURCE")" != "$TEMP/SOURCES" ]]; then
+    cp "$SOURCE" "$TEMP/SOURCES/" || die "Could not copy source $SOURCE to $TEMP/SOURCES"
+fi
+cp "$SOURCE_NETWORKMANAGER_CONF" "$TEMP/SOURCES/NetworkManager.conf" || die "Could not copy source $SOURCE_NETWORKMANAGER_CONF to $TEMP/SOURCES"
+cp "$SOURCE_CONFIG_SERVER" "$TEMP/SOURCES/00-server.conf" || die "Could not copy source $SOURCE_CONFIG_SERVER to $TEMP/SOURCES"
+cp "$SOURCE_CONFIG_CONNECTIVITY_FEDORA" "$TEMP/SOURCES/20-connectivity-fedora.conf" || die "Could not copy source $SOURCE_CONFIG_CONNECTIVITY_FEDORA to $TEMP/SOURCES"
+cp "$SOURCE_CONFIG_CONNECTIVITY_REDHAT" "$TEMP/SOURCES/20-connectivity-redhat.conf" || die "Could not copy source $SOURCE_CONFIG_CONNECTIVITY_REDHAT to $TEMP/SOURCES"
+cp "$SOURCE_CONFIG_WIFI_MAC_ADDR" "$TEMP/SOURCES/22-wifi-mac-addr.conf" || die "Could not copy source $SOURCE_CONFIG_WIFI_MAC_ADDR to $TEMP/SOURCES"
+cp "$SOURCE_SYSCTL_RP_FILTER_REDHAT" "$TEMP/SOURCES/70-nm-connectivity.conf" || die "Could not copy source $SOURCE_SYSCTL_RP_FILTER_REDHAT to $TEMP/SOURCES"
+cp "$SOURCE_README_IFCFG_FILES" "$TEMP/SOURCES/readme-ifcfg-rh.txt" || die "Could not copy source $SOURCE_README_IFCFG_FILES to $TEMP/SOURCES"
+cp "$SOURCE_README_IFCFG_MIGRATED" "$TEMP/SOURCES/readme-ifcfg-rh-migrated.txt" || die "Could not copy source $SOURCE_README_IFCFG_MIGRATED to $TEMP/SOURCES"
+
+write_changelog
+
+sed -e "s/__VERSION__/$VERSION/g" \
+    -e "s/__RELEASE_VERSION__/$RELEASE_VERSION/g" \
+    -e "s/__SNAPSHOT__/$SNAPSHOT/g" \
+    -e "s/__COMMIT__/$COMMIT/g" \
+    -e "s/__COMMIT_FULL__/$COMMIT_FULL/g" \
+    -e "s/__SNAPSHOT__/$SNAPSHOT/g" \
+    -e "s/__SOURCE1__/$(basename "$SOURCE")/g" \
+    -e "s/__BCOND_DEFAULT_DEBUG__/$BCOND_DEFAULT_DEBUG/g" \
+    -e "s/__BCOND_DEFAULT_LTO__/${BCOND_DEFAULT_LTO:-"%{nil}"}/g" \
+    -e "s/__BCOND_DEFAULT_TEST__/$BCOND_DEFAULT_TEST/g" \
+   "$SPECFILE" |
+sed -e "/^__CHANGELOG__$/ \
+        {
+            r $TEMPBASE/SOURCES/CHANGELOG
+            d
+        }" > "$TEMPSPEC" || die "Error reading spec file"
+
+case "$BUILDTYPE" in
+    "SRPM")
+        RPM_BUILD_OPTION=-bs
+        ;;
+    *)
+        RPM_BUILD_OPTION=-ba
+        ;;
+esac
+
+rpmbuild --define "_topdir $TEMP" $RPM_BUILD_OPTION "$TEMPSPEC" $NM_RPMBUILD_ARGS || die "ERROR: rpmbuild FAILED"
+
+LS_EXTRA=()
+
+if [ "$SIGN_SOURCE" = 1 ]; then
+    SIGNKEY="$(git config --get user.signingkey)"
+    if [ "$SIGNKEY" != "" ]; then
+        SIGNKEY="--local-user $(printf '%q' "$SIGNKEY")"
+    fi
+    gpg $SIGNKEY --output "$SOURCE.sig" --armor --detach-sig "$SOURCE" || die "ERROR: failure to sign $SOURCE"
+    LS_EXTRA+=("$SOURCE.sig")
+fi
+
+ln -snf "$TEMPBASE" ./latest
+TEMP_LATEST="$(readlink -f .)"/latest
+
+LOG
+LOG
+LOG "Finished with success."
+LOG
+LOG "See \"$TEMP_LATEST/\" which symlinks to \"$TEMPBASE\""
+LOG
+LOG "Result:"
+ls -dla \
+    "$TEMP_LATEST" \
+    "$SOURCE" \
+    "${LS_EXTRA[@]}" \
+    "$(dirname "$TEMP_LATEST")/$TEMPBASE/" \
+    "$TEMP_LATEST"/RPMS/*/ \
+    "$TEMP_LATEST"/RPMS/*/*.rpm \
+    "$TEMP_LATEST"/SRPMS/ \
+    "$TEMP_LATEST"/SRPMS/*.rpm \
+    2>/dev/null | sed 's/^/    /'
+LOG
+if [[ "$BUILDTYPE" == "SRPM" ]]; then
+    LOG sudo $(command -v dnf &>/dev/null && echo dnf builddep || echo yum-builddep) $TEMP_LATEST/SRPMS/*.src.rpm
+    LOG
+else
+    LOG "sudo $(command -v dnf &>/dev/null && echo dnf || echo yum) install '$TEMP_LATEST/RPMS'/*/*.rpm"
+    LOG
+fi
+
+if [[ "$DO_RELEASE" == 1 ]]; then
+    LOG "RELEASE \"$SOURCE\" :"
+    for c in md5 sha1 sha256 sha512; do
+        LOG "$(printf '%8s: %s' "$c" $("${c}sum" "$SOURCE" | sed 's/ .*//'))"
+    done
+    LOG
+fi
diff --git a/contrib/fedora/rpm/build_clean.sh b/contrib/fedora/rpm/build_clean.sh
new file mode 100755
index 00000000..d06d4341
--- /dev/null
+++ b/contrib/fedora/rpm/build_clean.sh
@@ -0,0 +1,303 @@
+#!/bin/bash
+
+
+die() {
+    echo "$*" >&2
+    exit 1
+}
+
+usage() {
+    echo "USAGE: $0 [-h|--help|-?|help] [-f|--force] [-c|--clean] [-S|--srpm] [-g|--git] [-Q|--quick] [-N|--no-dist] [[-w|--with OPTION] ...] [[-W|--without OPTION] ...]"
+    echo
+    echo "Does all the steps from a clean git working directory to an RPM of NetworkManager"
+    echo
+    echo "This is also the preferred way to create a distribution tarball for release:"
+    echo "  $ $0 -r"
+    echo
+    echo "Options:"
+    echo "  -f|--force: force build, even if working directory is not clean and has local modifications"
+    echo "  -c|--clean: run \`git-clean -fdx :/\` before build"
+    echo "  -S|--srpm: only build the SRPM"
+    echo "  -g|--git: create tarball from current git HEAD (skips make dist)"
+    echo "  -Q|--quick: only create the distribution tarball, without running checks"
+    echo "  -N|--no-dist: skip creating the source tarball if you already did \`make dist\`"
+    echo "  -m|--meson: (default) use meson to create the source tarball"
+    echo "  -A|--autotools: use autotools to create the source tarball"
+    echo "  -w|--with \$OPTION: pass --with \$OPTION to rpmbuild. For example --with debug"
+    echo "  -W|--without \$OPTION: pass --without \$OPTION to rpmbuild. For example --without debug"
+    echo "  -s|--snapshot TEXT: use TEXT as the snapshot version for the new package (overwrites \$NM_BUILD_SNAPSHOT environment)"
+    echo "  -r|--release: built a release tarball (this option must be alone)"
+    echo "  --default-for-debug \$OPTION: set the default for "debug" option in the generated spec file"
+    echo "  --default-for-lto \$OPTION: set the default for "lto" option in the generated spec file"
+    echo "  --default-for-test \$OPTION: set the default for "test" option in the generated spec file"
+}
+
+in_set() {
+    local v="$1"
+    shift
+    for v2; do
+        test "$v" = "$v2" && return 0
+    done
+    return 1
+}
+
+ORIGDIR="$(readlink -f "$PWD")"
+SCRIPTDIR="$(dirname "$(readlink -f "$0")")"
+GITDIR="$(cd "$SCRIPTDIR" && git rev-parse --show-toplevel || die "Could not get GITDIR")"
+
+
+[[ -x "$SCRIPTDIR"/build.sh ]] || die "could not find \"$SCRIPTDIR/build.sh\""
+
+cd "$GITDIR" || die "could not change to $GITDIR"
+
+IGNORE_DIRTY=0
+GIT_CLEAN=0
+QUICK=0
+NO_DIST=0
+WITH_LIST=()
+SOURCE_FROM_GIT=0
+SNAPSHOT="$NM_BUILD_SNAPSHOT"
+DO_RELEASE=0
+unset BCOND_DEFAULT_DEBUG
+unset BCOND_DEFAULT_LTO
+unset BCOND_DEFAULT_TEST
+
+ADD_WITH_TEST=1
+
+NARGS=$#
+
+while [[ $# -gt 0 ]]; do
+    A="$1"
+    shift
+    case "$A" in
+        -h|--help|-\?|help)
+            usage
+            exit 0
+            ;;
+        -f|--force)
+            IGNORE_DIRTY=1
+            ;;
+        -r|--release)
+            [[ $NARGS -eq 1 ]] || die "--release option must be alone"
+            export NMTST_CHECK_GTK_DOC=1
+            BUILDTYPE=SRPM
+            DO_RELEASE=1
+            ;;
+        -c|--clean)
+            GIT_CLEAN=1
+            ;;
+        -S|--srpm)
+            BUILDTYPE=SRPM
+            ;;
+        -s|--snapshot)
+            [[ $# -gt 0 ]] || die "Missing argument to $A"
+            SNAPSHOT="$1"
+            shift
+            ;;
+        -g|--git)
+            NO_DIST=1
+            IGNORE_DIRTY=1
+            SOURCE_FROM_GIT=1
+            ;;
+        -m|--meson)
+            [ "$USE_AUTOTOOLS" = 1 ] && die "conflicting argument: $A when building with autotools is requested";
+            USE_MESON=1
+            ;;
+        -A|--autotools)
+            [ "$USE_MESON" = 1 ] && die "conflicting argument: $A when building with meson is explicitly requested";
+            USE_AUTOTOOLS=1
+            ;;
+        -Q|--quick)
+            QUICK=1
+            ;;
+        -N|--no-dist)
+            NO_DIST=1
+            IGNORE_DIRTY=1
+            SOURCE_FROM_GIT=0
+            ;;
+        -w|--with)
+            [[ $# -gt 0 ]] || die "Missing argument to $A"
+            WITH_LIST=("${WITH_LIST[@]}" "--with" "$1")
+            case "$1" in
+                debug)
+                    [[ -z ${BCOND_DEFAULT_DEBUG+.} ]] && BCOND_DEFAULT_DEBUG=1
+                    ;;
+                lto)
+                    [[ -z ${BCOND_DEFAULT_LTO+.} ]] && BCOND_DEFAULT_LTO=1
+                    ;;
+                test)
+                    ADD_WITH_TEST=0
+                    [[ -z ${BCOND_DEFAULT_TEST+.} ]] && BCOND_DEFAULT_TEST=1
+                    ;;
+            esac
+            shift
+            ;;
+        -W|--without)
+            [[ $# -gt 0 ]] || die "Missing argument to $A"
+            WITH_LIST=("${WITH_LIST[@]}" "--without" "$1")
+            case "$1" in
+                debug)
+                    [[ -z ${BCOND_DEFAULT_DEBUG+.} ]] && BCOND_DEFAULT_DEBUG=0
+                    ;;
+                lto)
+                    [[ -z ${BCOND_DEFAULT_LTO+.} ]] && BCOND_DEFAULT_LTO=0
+                    ;;
+                test)
+                    ADD_WITH_TEST=0
+                    [[ -z ${BCOND_DEFAULT_TEST+.} ]] && BCOND_DEFAULT_TEST=0
+                    ;;
+            esac
+            shift
+            ;;
+        --no-auto-with-test)
+            # by default, the script adds "-w test" (unless the command line contains
+            # "-w test" or "-W test"). This flags allows to suppress that automatism.
+            # It's really only useful to test the spec file's internal default for the
+            # "test" option. Otherwise, you can always just explicitly select "-w test"
+            # or "-W test".
+            ADD_WITH_TEST=0
+            ;;
+        --default-for-debug)
+            [[ $# -gt 0 ]] || die "Missing argument to $A"
+            in_set "$1" "" 0 1 || die "invalid argument $A \"$1\""
+            BCOND_DEFAULT_DEBUG="$1"
+            shift
+            ;;
+        --default-for-lto)
+            [[ $# -gt 0 ]] || die "Missing argument to $A"
+            in_set "$1" "" 0 1 || die "invalid argument $A \"$1\""
+            BCOND_DEFAULT_LTO="$1"
+            shift
+            ;;
+        --default-for-test)
+            [[ $# -gt 0 ]] || die "Missing argument to $A"
+            in_set "$1" "" 0 1 || die "invalid argument $A \"$1\""
+            BCOND_DEFAULT_TEST="$1"
+            shift
+            ;;
+        *)
+            usage
+            die "Unexpected argument \"$A\""
+            ;;
+    esac
+done
+
+if [[ $GIT_CLEAN == 1 ]]; then
+    git clean -fdx :/
+fi
+
+if [[ $IGNORE_DIRTY != 1 ]]; then
+    # check for a clean working directory.
+    # We ignore the /contrib directory, because this is where the automation
+    # scripts and the build results will be.
+    if [[ "x$(LANG=C git clean -ndx | grep '^Would \(remove contrib/\|skip repository libgsystem/\).*$' -v)" != x ]]; then
+        die "The working directory is not clean. Refuse to run. Try \`$0 --force\`, \`$0 --clean\`, or \`git clean -e :/contrib -dx -n\`"
+    fi
+    if [[ "x$(git status --porcelain)" != x ]]; then
+        die "The working directory has local changes. Refuse to run. Try \`$0 --force\`"
+    fi
+fi
+
+get_version_meson() {
+    meson introspect "$GITDIR/build" --projectinfo | jq -r .version
+}
+
+if [[ $NO_DIST != 1 ]]; then
+    if [[ $USE_AUTOTOOLS != 1 ]]; then
+            meson setup "$GITDIR/build" \
+                --prefix=/usr \
+                --bindir=/usr/bin \
+                --sbindir=/usr/sbin \
+                --sysconfdir=/etc \
+                --datadir=/usr/share \
+                --includedir=/usr/include \
+                --libdir=/usr/lib \
+                --libexecdir=/usr/libexec \
+                --localstatedir=/var \
+                --sharedstatedir=/var/lib \
+                --mandir=/usr/share/man \
+                --infodir=/usr/share/info \
+                -Ddocs=true \
+                -Dintrospection=true \
+                -Difcfg_rh=true \
+                -Difupdown=true \
+                -Dconfig_logging_backend_default=syslog \
+                -Dconfig_wifi_backend_default=wpa_supplicant \
+                -Dlibaudit=yes-disabled-by-default \
+                -Dpolkit=true \
+                -Dnm_cloud_setup=true \
+                -Dconfig_dhcp_default=internal \
+                -Dconfig_dns_rc_manager_default=auto \
+                -Diptables=/usr/sbin/iptables \
+                -Dnft=/usr/bin/nft \
+                || die "Error meson setup"
+
+            VERSION="${VERSION:-$(get_version_meson || die "Could not read $VERSION")}"
+            if [[ $QUICK == 1 ]]; then
+                meson dist --allow-dirty -C "$GITDIR/build/" --no-tests || die "Error meson dist"
+            else
+                meson dist --allow-dirty -C "$GITDIR/build/" || die "Error meson dist with tests"
+            fi
+            export SOURCE="$(ls -1 "$GITDIR/build/meson-dist/NetworkManager-${VERSION}.tar.xz" 2>/dev/null | head -n1)"
+    else
+        ./autogen.sh \
+            --with-runstatedir=/run \
+            --program-prefix= \
+            --prefix=/usr \
+            --exec-prefix=/usr \
+            --bindir=/usr/bin \
+            --sbindir=/usr/sbin \
+            --sysconfdir=/etc \
+            --datadir=/usr/share \
+            --includedir=/usr/include \
+            --libdir=/usr/lib \
+            --libexecdir=/usr/libexec \
+            --localstatedir=/var \
+            --sharedstatedir=/var/lib \
+            --mandir=/usr/share/man \
+            --infodir=/usr/share/info \
+            \
+            --disable-dependency-tracking \
+            --enable-gtk-doc \
+            --enable-introspection \
+            --enable-ifcfg-rh \
+            --enable-ifupdown \
+            --with-config-logging-backend-default=syslog \
+            --with-config-wifi-backend-default=wpa_supplicant \
+            --with-libaudit=yes-disabled-by-default \
+            --enable-polkit=yes \
+            --with-nm-cloud-setup=yes \
+            --with-config-dhcp-default=internal \
+            --with-config-dns-rc-manager-default=auto \
+            \
+            --with-iptables=/usr/sbin/iptables \
+            --with-nft=/usr/sbin/nft \
+            \
+            || die "Error autogen.sh"
+        if [[ $QUICK == 1 ]]; then
+            make dist -j 7 || die "Error make dist"
+        else
+            make distcheck -j 7 || die "Error make distcheck"
+        fi
+    fi
+fi
+
+if [[ "$ADD_WITH_TEST" == 1 ]]; then
+    WITH_LIST=("${WITH_LIST[@]}" "--with" "test")
+fi
+
+if [[ "$USE_AUTOTOOLS" != 1 ]]; then
+    WITH_LIST=("${WITH_LIST[@]}" "--with" "meson")
+fi
+
+export SOURCE_FROM_GIT
+export BUILDTYPE
+export NM_RPMBUILD_ARGS="${WITH_LIST[@]}"
+export SNAPSHOT
+export DO_RELEASE
+export BCOND_DEFAULT_DEBUG="$BCOND_DEFAULT_DEBUG"
+export BCOND_DEFAULT_LTO="$BCOND_DEFAULT_LTO"
+export BCOND_DEFAULT_TEST="$BCOND_DEFAULT_TEST"
+
+"$SCRIPTDIR"/build.sh
+
diff --git a/contrib/fedora/rpm/configure-for-system.sh b/contrib/fedora/rpm/configure-for-system.sh
new file mode 100755
index 00000000..acf5eb9c
--- /dev/null
+++ b/contrib/fedora/rpm/configure-for-system.sh
@@ -0,0 +1,526 @@
+#!/bin/bash
+
+# Run configure/meson for NetworkManager in a way similar to how an RPM build does it.
+# The effect is, that if you do `make install`, that it will overwrite the files that
+# you'd usually get by installing the NetworkManager RPM. Also, it means you can afterwards
+# systemctl restart NetworkManager.
+
+die() {
+    printf "%s\n" "$*"
+    exit 1
+}
+
+BASE_DIR="$(cd "$(dirname "$BASH_SOURCE")"; git rev-parse --show-toplevel)"
+
+cd "$BASE_DIR" || die "Cannot cd to base directory"
+
+vars() {
+    sed -e '1,/[P]VARS/!d' "$BASH_SOURCE" | sed -n 's/^'"$1"'_\([^=]*\)=.*/\1/p'
+}
+
+vars_with_vals() {
+    echo "Variables:"
+    for v in $(vars P); do
+        printf "  %s=%q\n" "$v" "$(eval "echo \"\$P_$v\"")"
+    done
+    echo "Directories:"
+    for v in $(vars D); do
+        printf "  %s=%q\n" "$v" "$(eval "echo \"\$D_$v\"")"
+    done
+}
+
+usage() {
+    echo "$ $0 [-m|--meson] [-a|--autotools] [-s|--show] [-B|--no-build] [-h|--help]"
+    echo ""
+    echo "Configure NetworkManager in a way that is similar to when building"
+    echo "RPMs of NetworkManager for Fedora/RHEL. The effect is that \`make install\`"
+    echo "will overwrite the files in /usr that you installed via the package management"
+    echo "systemd. Also, subsequent \`systemctl restart NetworkManager\` works."
+    echo "You don't want to do this on your real system, because it messes up your"
+    echo "installation"
+    echo
+
+    vars_with_vals
+}
+
+get_version() {
+    local major minor micro
+    local F="./configure.ac"
+
+    vars="$(sed -n 's/^m4_define(\[nm_\(major\|minor\|micro\)_version\], *\[\([0-9]\+\)\]) *$/local \1='\''\2'\''/p' "$F" 2>/dev/null)"
+    eval "$vars"
+
+    [[ -n "$major" && -n "$minor" && "$micro" ]] || return 1
+    echo "$major.$minor.$micro"
+}
+
+bool() {
+    case "$1" in
+        1|y|Y|yes|Yes|YES|true|True|TRUE)
+            return 0
+            ;;
+        0|n|N|no|No|NO|false|False|FALSE)
+            return 1
+            ;;
+        *)
+            local re='^[0-9]+$'
+            [[ $1 =~ $re ]] && test "$1" -gt 0 && return 0
+            [ "$#" -le "1" ] && return 1
+            shift
+            bool "$@"
+            return $?
+            ;;
+    esac
+}
+
+bool_true() {
+    if bool "$@"; then
+        echo true
+    else
+        echo false
+    fi
+}
+
+bool_not_true() {
+    if bool "$@"; then
+        echo false
+    else
+        echo true
+    fi
+}
+
+bool_enable() {
+    if bool "$@"; then
+        echo enable
+    else
+        echo disable
+    fi
+}
+
+bool_not_enable() {
+    if bool "$@"; then
+        echo disable
+    else
+        echo enable
+    fi
+}
+
+bool_yes() {
+    if bool "$@"; then
+        echo yes
+    else
+        echo no
+    fi
+}
+
+bool_not_yes() {
+    if bool "$@"; then
+        echo no
+    else
+        echo yes
+    fi
+}
+
+args_enable() {
+    local cond="$1"
+    local a
+    shift
+    if bool "$cond" ; then
+        for a; do
+            printf "%q\n" "$a"
+        done
+    fi
+}
+
+show_cmd() {
+    local a
+    local sep=
+
+    for a; do
+        printf '%s%q' "$sep" "$a"
+        sep=' '
+    done
+    printf '\n'
+}
+
+SHOW_CMD=
+
+P_NOBUILD="${NOBUILD-0}"
+
+P_DEBUG="${DEBUG-1}"
+
+P_BUILD_TYPE="${BUILD_TYPE-}"
+P_CFLAGS="${CFLAGS-}"
+P_CC="${CC-$((! command -v gcc && command -v clang) &>/dev/null && echo clang || echo gcc)}"
+
+P_RHEL="${RHEL-}"
+P_FEDORA="${FEDORA-}"
+
+P_CONFIG_PLUGINS_DEFAULT_IFCFG_RH="${CONFIG_PLUGINS_DEFAULT_IFCFG_RH-}"
+P_CRYPTO="${CRYPTO-}"
+P_DBUS_SYS_DIR="${DBUS_SYS_DIR-}"
+P_DHCP_DEFAULT="${DHCP_DEFAULT-}"
+P_DNS_RC_MANAGER_DEFAULT="${DNS_RC_MANAGER_DEFAULT-}"
+P_EBPF_ENABLED="${EBPF_ENABLED-no}"
+P_FIREWALLD_ZONE="${FIREWALLD_ZONE-}"
+P_IWD="${IWD-}"
+P_LOGGING_BACKEND_DEFAULT="${LOGGING_BACKEND_DEFAULT-}"
+P_LTO="${LTO-0}"
+P_MODEM_MANAGER_1="${MODEM_MANAGER_1-}"
+P_TEST="${TEST-1}"
+P_SILENT_RULES="${SILENT_RULES-1}"
+
+P_VERSION="${VERSION:-$(get_version)}"
+P_RELEASE="${RELEASE:-$(git rev-list HEAD | wc -l).test}"
+
+P_REGEN_DOCS="${REGEN_DOCS-1}"
+P_SANITIZER="${SANITIZER-0}"
+
+P_WIFI="${WIFI-1}"
+P_WWAN="${WWAN-1}"
+P_TEAM="${TEAM-1}"
+P_BLUETOOTH="${BLUETOOTH-1}"
+P_NMTUI="${NMTUI-1}"
+P_NM_CLOUD_SETUP="${NM_CLOUD_SETUP-1}"
+P_OVS="${OVS-1}"
+P_PPP="${PPP-1}"
+
+P_PPP_VERSION="${PPP_VERSION-}"
+
+D_PREFIX="$(rpm --eval "%{_prefix}")"
+D_BINDIR="$(rpm --eval "%{_bindir}")"
+D_SBINDIR="$(rpm --eval "%{_sbindir}")"
+D_LIBDIR="$(rpm --eval "%{_libdir}")"
+D_LIBEXECDIR="$(rpm --eval "%{_libexecdir}")"
+D_INCLUDEDIR="$(rpm --eval "%{_includedir}")"
+D_DATADIR="$(rpm --eval "%{_datadir}")"
+D_RUNDIR="$(rpm --eval "%{_rundir}")"
+D_MANDIR="$(rpm --eval "%{_mandir}")"
+D_INFODIR="$(rpm --eval "%{_infodir}")"
+D_SYSCONFDIR="$(rpm --eval "%{_sysconfdir}")"
+D_LOCALSTATEDIR="$(rpm --eval "%{_localstatedir}")"
+D_SHAREDSTATEDIR="$(rpm --eval "%{_sharedstatedir}")"
+
+#PVARS
+
+if [ -z "$P_FEDORA" -a -z "$P_RHEL" ] ; then
+    x="$(grep -q "ID=fedora" /etc/os-release && sed -n 's/VERSION_ID=//p' /etc/os-release)"
+    if test "$x" -gt 0 ; then
+        P_FEDORA="$x"
+        P_RHEL=0
+    else
+        x="$(grep -q "ID=fedora" /etc/os-release && sed -n 's/VERSION_ID=//p' /etc/os-release)"
+        if test "$x" -gt 0 ; then
+            P_FEDORA=0
+            P_RHEL="$x"
+        fi
+    fi
+fi
+test -z "$P_FEDORA" && P_FEDORA=0
+test -z "$P_RHEL" && P_RHEL=0
+
+test "$P_FEDORA" -gt 0 -o "$P_RHEL" -gt 0 || die "FEDORA/RHEL variables unset"
+
+if [ -z "$P_PPP_VERSION" ] ; then
+    P_PPP_VERSION="$(sed -n 's/^#define\s*VERSION\s*"\([^\s]*\)"$/\1/p' "$D_INCLUDEDIR/pppd/patchlevel.h" 2>/dev/null | grep . || echo bad)"
+fi
+
+if [ -z "$P_CRYPTO" ] ; then
+    if [ "$P_FEDORA" -ge 29 -o "$P_RHEL" -ge 8  ] ; then
+        P_CRYPTO=gnutls
+    else
+        P_CRYPTO=nss
+    fi
+fi
+
+if [ -z "$P_CONFIG_PLUGINS_DEFAULT_IFCFG_RH" ] ; then
+    if [ "$P_FEDORA" -ge 33 -o  "$P_RHEL" -ge 9 ] ; then
+        P_CONFIG_PLUGINS_DEFAULT_IFCFG_RH=0
+    else
+        P_CONFIG_PLUGINS_DEFAULT_IFCFG_RH=1
+    fi
+fi
+
+
+if [ -z "$P_DBUS_SYS_DIR" ] ; then
+    if [ "$P_FEDORA" -ge 1 -o  "$P_RHEL" -ge 8 ] ; then
+        P_DBUS_SYS_DIR="$D_DATADIR/dbus-1/system.d"
+    else
+        P_DBUS_SYS_DIR="$D_SYSCONFDIR/dbus-1/system.d"
+    fi
+fi
+
+if [ -z "$P_DNS_RC_MANAGER_DEFAULT" ] ; then
+    if [ "$P_FEDORA" -ge 1 -o  "$P_RHEL" -ge 9 ] ; then
+        P_DNS_RC_MANAGER_DEFAULT=auto
+    elif [ "$P_FEDORA" -ge 1 -o  "$P_RHEL" -ge 8 ] ; then
+        P_DNS_RC_MANAGER_DEFAULT=symlink
+    else
+        P_DNS_RC_MANAGER_DEFAULT=file
+    fi
+fi
+
+if [ -z "$P_LOGGING_BACKEND_DEFAULT" ] ; then
+    if [ "$P_FEDORA" -ge 1 -o  "$P_RHEL" -ge 8 ] ; then
+        P_LOGGING_BACKEND_DEFAULT=journal
+    else
+        P_LOGGING_BACKEND_DEFAULT=syslog
+    fi
+fi
+
+if [ -z "$P_DHCP_DEFAULT" ] ; then
+    if [ "$P_FEDORA" -ge 31 -o "$P_RHEL" -ge 8 ] ; then
+        P_DHCP_DEFAULT=internal
+    else
+        P_DHCP_DEFAULT=dhclient
+    fi
+fi
+
+if [ -z "$P_FIREWALLD_ZONE" ] ; then
+    if [ "$P_FEDORA" -ge 32 -o "$P_RHEL" -ge 8 ] ; then
+        P_FIREWALLD_ZONE=1
+    else
+        P_FIREWALLD_ZONE=0
+    fi
+fi
+
+if [ -z "$P_IWD" ] ; then
+    if [ "$P_RHEL" -ge 1 ] ; then
+        P_IWD=0
+    else
+        P_IWD=1
+    fi
+fi
+
+if [ -z "$P_MODEM_MANAGER_1" ] ; then
+    if bool "$P_BLUETOOTH" || bool "$P_WWAN" ; then
+        P_MODEM_MANAGER_1=1
+    else
+        P_MODEM_MANAGER_1=0
+    fi
+fi
+
+if bool "$P_DEBUG" ; then
+    P_CFLAGS="-g -Og -fexceptions${P_CFLAGS:+ }$P_CFLAGS"
+else
+    P_CFLAGS="-g -O2 -fexceptions${P_CFLAGS:+ }$P_CFLAGS"
+fi
+
+if [ -z "$P_BUILD_TYPE" ] ; then
+    if [ -d ./build -a ! -f ./configure ] ; then
+        P_BUILD_TYPE=meson
+    elif [ ! -d ./build -a -f ./configure ] ; then
+        P_BUILD_TYPE=autotools
+    else
+        P_BUILD_TYPE=autotools
+    fi
+fi
+
+while [[ $# -gt 0 ]] ; do
+    A="$1"
+    shift
+    case "$A" in
+        --meson|-m)
+            P_BUILD_TYPE=meson
+            ;;
+        --autotools|-a)
+            P_BUILD_TYPE=autotools
+            ;;
+        -s|--show)
+            SHOW_CMD=show_cmd
+            ;;
+        -h|help|-help|--help)
+            usage
+            exit 0
+            ;;
+        -B|--no-build)
+            P_NOBUILD=1
+            ;;
+        *)
+            usage
+            exit 1
+            ;;
+    esac
+done
+
+vars_with_vals
+
+if [ "$P_BUILD_TYPE" == meson ] ; then
+    MESON_RECONFIGURE=
+    if test -d "./build/" ; then
+        MESON_RECONFIGURE="--reconfigure"
+    fi
+
+    $SHOW_CMD \
+    env \
+    CC="$P_CC" \
+    CFLAGS="$P_CFLAGS" \
+    meson \
+        --buildtype=plain \
+        --prefix="$D_PREFIX" \
+        --libdir="$D_LIBDIR" \
+        --libexecdir="$D_LIBEXECDIR" \
+        --bindir="$D_BINDIR" \
+        --sbindir="$D_SBINDIR" \
+        --includedir="$D_INCLUDEDIR" \
+        --datadir="$D_DATADIR" \
+        --mandir="$D_MANDIR" \
+        --infodir="$D_INFODIR" \
+        --localedir="$D_DATADIR"/locale \
+        --sysconfdir="$D_SYSCONFDIR" \
+        --localstatedir="$D_LOCALSTATEDIR" \
+        --sharedstatedir="$D_SHAREDSTATEDIR" \
+        --wrap-mode=nodownload \
+        --auto-features=enabled \
+        \
+        build \
+        \
+        $MESON_RECONFIGURE \
+        \
+        -Db_ndebug=false \
+        --warnlevel 2 \
+        $(args_enable "$P_TEST" --werror) \
+        -Dnft="${D_SBINDIR}/nft" \
+        -Diptables="${D_SBINDIR}/iptables" \
+        -Ddhclient="${D_SBINDIR}/dhclient" \
+        -Ddhcpcanon=no \
+        -Ddhcpcd=no \
+        -Dconfig_dhcp_default="$P_DHCP_DEFAULT" \
+        "-Dcrypto=$P_CRYPTO" \
+        $(args_enable "$P_DEBUG"                    -Dmore_logging=true  -Dmore_asserts=10000) \
+        $(args_enable "$(bool_not_true "$P_DEBUG")" -Dmore_logging=false -Dmore_asserts=0    ) \
+        -Dld_gc=true \
+        -Db_lto="$(bool_true "$P_LTO")" \
+        -Dlibaudit=yes-disabled-by-default \
+        -Dmodem_manager="$(bool_true "$P_MODEM_MANAGER_1")" \
+        $(args_enable "$P_WIFI"                    -Dwifi=true  -Dwext="$(bool_true "$P_FEDORA")") \
+        $(args_enable "$(bool_not_true "$P_WIFI")" -Dwifi=false                                  ) \
+        -Diwd="$(bool_true "$P_IWD")" \
+        -Dbluez5_dun="$(bool_true "$P_BLUETOOTH")" \
+        -Dnmtui="$(bool_true "$P_NMTUI")" \
+        -Dnm_cloud_setup="$(bool_true "$P_NM_CLOUD_SETUP")" \
+        -Dvapi=true \
+        -Dintrospection=true \
+        -Ddocs="$(bool_true "$P_REGEN_DOCS")" \
+        -Dteamdctl="$(bool_true "$P_TEAM")" \
+        -Dovs="$(bool_true "$P_OVS")" \
+        -Dselinux=true \
+        -Dpolkit=true  \
+        -Dconfig_auth_polkit_default=true \
+        -Dmodify_system=true \
+        -Dconcheck=true \
+        -Dlibpsl="$(bool_true "$P_FEDORA")" \
+        -Debpf="$(bool_true "$P_EBPF_ENABLED")" \
+        -Dsession_tracking=systemd \
+        -Dsuspend_resume=systemd \
+        -Dsystemdsystemunitdir=/usr/lib/systemd/system \
+        -Dsystem_ca_path=/etc/pki/tls/cert.pem \
+        -Ddbus_conf_dir="$P_DBUS_SYS_DIR" \
+        -Dtests=yes \
+        -Dvalgrind=no \
+        -Difcfg_rh=true \
+        -Difupdown=false \
+        $(args_enable "$P_PPP"                    -Dppp=true  -Dpppd="$D_SBINDIR/pppd" -Dpppd_plugin_dir="$D_LIBDIR/pppd/$P_PPP_VERSION") \
+        $(args_enable "$(bool_not_true "$P_PPP")" -Dppp=false                                                                           ) \
+        -Dfirewalld_zone="$(bool_true "$P_FIREWALLD_ZONE}")" \
+        -Ddist_version="$P_VERSION-$P_RELEASE" \
+        $(args_enable "$P_CONFIG_PLUGINS_DEFAULT_IFCFG_RH" -Dconfig_plugins_default=ifcfg-rh) \
+        -Dresolvconf=no \
+        -Dnetconfig=no \
+        -Dconfig_dns_rc_manager_default="$P_DNS_RC_MANAGER_DEFAULT" \
+        -Dconfig_logging_backend_default="$P_LOGGING_BACKEND_DEFAULT" \
+        ;
+else
+    if ! test -x ./configure ; then
+        if [ -z "$SHOW_CMD" ]; then
+            NOCONFIGURE=yes ./autogen.sh
+        fi
+    fi
+    $SHOW_CMD \
+    ./configure \
+        --build=x86_64-redhat-linux-gnu \
+        --host=x86_64-redhat-linux-gnu \
+        --program-prefix= \
+        --prefix="$D_PREFIX" \
+        --exec-prefix=/usr \
+        --bindir="$D_BINDIR" \
+        --sbindir="$D_SBINDIR" \
+        --sysconfdir="$D_SYSCONFDIR" \
+        --datadir="$D_DATADIR" \
+        --includedir="$D_INCLUDEDIR" \
+        --libdir="$D_LIBDIR" \
+        --libexecdir="$D_LIBEXECDIR" \
+        --localstatedir="$D_LOCALSTATEDIR" \
+        --sharedstatedir="$D_SHAREDSTATEDIR" \
+        --mandir="$D_MANDIR" \
+        --infodir="$D_INFODIR" \
+        \
+        CC="$P_CC" \
+        CFLAGS="$P_CFLAGS" \
+        \
+        --enable-dependency-tracking=yes \
+        \
+        --with-runstatedir="$D_RUNDIR" \
+        --enable-silent-rules="$(bool_yes "$P_SILENT_RULES")" \
+        --enable-static=no \
+        --with-nft="${D_SBINDIR}/nft" \
+        --with-iptables="${D_SBINDIR}/iptables" \
+        --with-dhclient="${D_SBINDIR}/dhclient" \
+        --with-dhcpcd=no \
+        --with-dhcpcanon=no \
+        --with-config-dhcp-default="$P_DHCP_DEFAULT" \
+        --with-crypto="$P_CRYPTO" \
+        $(args_enable "$P_SANITIZER"                    --with-address-sanitizer=exec --enable-undefined-sanitizer="$( (bool "$P_FEDORA" || test "$P_RHEL" -ge 8) && echo yes || echo no)" ) \
+        $(args_enable "$(bool_not_true "$P_SANITIZER")" --with-address-sanitizer=no   --enable-undefined-sanitizer=no                                                                            ) \
+        $(args_enable "$P_DEBUG"                    --enable-more-logging=yes --with-more-asserts=10000) \
+        $(args_enable "$(bool_not_true "$P_DEBUG")" --enable-more-logging=no  --with-more-asserts=0    ) \
+        --enable-ld-gc=yes \
+        --enable-lto="$(bool_yes "$P_LTO")" \
+        --with-libaudit=yes-disabled-by-default \
+        --with-modem-manager-1="$(bool_yes "$P_MODEM_MANAGER_1")" \
+        $(args_enable "$P_WIFI"                    --enable-wifi=yes --with-wext="$(bool_yes "$P_FEDORA")") \
+        $(args_enable "$(bool_not_true "$P_WIFI")" --enable-wifi=no                                       ) \
+        --with-iwd="$(bool_yes "$P_IWD")" \
+        --enable-bluez5-dun="$(bool_yes "$P_BLUETOOTH")" \
+        --with-nmtui="$(bool_yes "$P_NMTUI")" \
+        --with-nm-cloud-setup="$(bool_yes "$P_NM_CLOUD_SETUP")" \
+        --enable-vala=yes \
+        --enable-introspection=yes \
+        --enable-gtk-doc="$(bool_yes "$P_REGEN_DOCS")" \
+        --enable-teamdctl="$(bool_yes "$P_TEAM")" \
+        --enable-ovs="$(bool_yes "$P_OVS")" \
+        --with-selinux=yes \
+        --enable-polkit=yes \
+        --enable-modify-system=yes \
+        --enable-concheck=yes \
+        --with-libpsl="$(bool_yes "$P_FEDORA")" \
+        --with-ebpf="$(bool_yes "$P_EBPF_ENABLED")" \
+        --with-session-tracking=systemd \
+        --with-suspend-resume=systemd \
+        --with-systemdsystemunitdir=/usr/lib/systemd/system \
+        --with-system-ca-path=/etc/pki/tls/cert.pem \
+        --with-dbus-sys-dir="$P_DBUS_SYS_DIR" \
+        --with-tests=yes \
+        --enable-more-warnings="$(bool "$P_TEST" && echo error || echo yes)" \
+        --with-valgrind=no \
+        --enable-ifcfg-rh=yes \
+        --enable-ifupdown=no \
+        $(args_enable "$P_PPP"                    --enable-ppp=yes --with-pppd="$D_SBINDIR/pppd" --with-pppd-plugin-dir="$D_LIBDIR/pppd/$P_PPP_VERSION") \
+        $(args_enable "$(bool_not_true "$P_PPP")" --enable-ppp=no                                                                                      ) \
+        --enable-firewalld-zone="$(bool_yes "$P_FIREWALLD_ZONE")" \
+        --with-dist-version="$P_VERSION-$P_RELEASE" \
+        $(args_enable "$P_CONFIG_PLUGINS_DEFAULT_IFCFG_RH" --with-config-plugins-default=ifcfg-rh) \
+        --with-resolvconf=no \
+        --with-netconfig=no \
+        --with-config-dns-rc-manager-default="$P_DNS_RC_MANAGER_DEFAULT" \
+        --with-config-logging-backend-default="$P_LOGGING_BACKEND_DEFAULT" \
+        ;
+fi
+
+if ! bool "$P_NOBUILD" ; then
+    if [ "$P_BUILD_TYPE" == meson ] ; then
+        $SHOW_CMD ninja -C build
+    else
+        $SHOW_CMD make -j 10
+    fi
+fi
diff --git a/contrib/fedora/rpm/mockbuild.sh b/contrib/fedora/rpm/mockbuild.sh
new file mode 100755
index 00000000..785aa254
--- /dev/null
+++ b/contrib/fedora/rpm/mockbuild.sh
@@ -0,0 +1,20 @@
+#!/bin/sh
+#
+# mockbuild.sh
+#
+# Generate SRPM from git tree and rebuild it using mock.
+
+SCRIPTDIR="$(dirname "$(readlink -f "$0")")"
+FEDORAVER=$(sed -E 's/.*([0-9]{2}).*/\1/g' /etc/fedora-release)
+ARCH=$(uname -m)
+SRPM=${SCRIPTDIR}/latest/SRPMS/NetworkManager*.src.rpm
+
+alias mock="mock -r fedora-${FEDORAVER}-${ARCH}"
+
+# Generate SRPM
+${SCRIPTDIR}/build_clean.sh --srpm --git
+
+# Rebuild SRPM
+mock --rebuild ${SRPM}
+
+exit
diff --git a/contrib/fedora/rpm/readme-ifcfg-rh-migrated.txt b/contrib/fedora/rpm/readme-ifcfg-rh-migrated.txt
new file mode 100644
index 00000000..aabbcc86
--- /dev/null
+++ b/contrib/fedora/rpm/readme-ifcfg-rh-migrated.txt
@@ -0,0 +1,84 @@
+NetworkManager was built to automatically migrate connection profiles in
+this directory to equivalent ones in keyfile format in directory
+/etc/NetworkManager/system-connections.
+
+You can check whether the migration is enabled via:
+
+ $ NetworkManager --print-config | grep migrate-ifcfg-rh
+
+In case it is enabled, all files in this directory are migrated at startup.
+
+To inspect where your connection files are currently stored use:
+
+ $ nmcli -f name,uuid,filename connection
+
+Background
+==========
+
+The ifcfg format is deprecated and will be removed in future releases. For
+more information see:
+
+https://lists.freedesktop.org/archives/networkmanager/2023-May/000103.html
+
+Connection profiles in keyfile format have many benefits. For example, this
+format is INI file-based and can easily be parsed and generated.
+
+Each section in NetworkManager keyfiles corresponds to a NetworkManager
+setting name as described in the nm-settings(5) and nm-settings-keyfile(5)
+man pages. Each key-value pair in a section is one of the properties listed
+in the settings specification of the man page.
+
+How to keep using ifcfg
+=======================
+
+If you want to keep using connection profiles in ifcfg format, you need to:
+
+ - disable the automatic migration to keyfile by setting
+   "migrate-ifcfg-rh=false" in the [main] section of NetworkManager
+   configuration;
+
+ - optionally, set "plugins=ifcfg-rh" in the [main] section of
+   NetworkManager configuration so that new profiles are created in ifcfg
+   format.
+
+At this point, you can migrate all your files back via
+
+  nmcli connection migrate --plugin ifcfg-rh
+
+Or, if you prefer to migrate only specific connections:
+
+  nmcli connection migrate --plugin ifcfg-rh <profile_name|UUID>
+
+Note that some connection types are not supported by the ifcfg plugin.
+
+Interface renaming
+==================
+
+Connection profiles stored in ifcfg-rh format support the renaming of
+interfaces via udev. This is done via a helper tool
+/usr/lib/udev/rename_device that is invoked by udev to parse the files
+in /etc/sysconfig/network-scripts; when the HWADDR and DEVICE
+variables are set, the interface that matches the MAC address in
+HWADDR is renamed to the name specified in DEVICE.
+
+Connections in keyfile format don't provide the same integration with
+udev. The renaming of interfaces must be configured directly in udev,
+for example by creating a file:
+
+  /etc/systemd/network/70-rename.link
+
+with content:
+
+  [Match]
+  MACAddress=00:11:22:33:44:56
+
+  [Link]
+  Name=ethernet1
+
+Alternatively, a udev rule can also be used, such as:
+
+  /etc/udev/rules.d/70-interface-names.rules
+
+with content:
+
+  SUBSYSTEM=="net",ACTION=="add",ATTR{address}=="00:11:22:33:44:56",ATTR{type}=="1",NAME="ethernet1"
diff --git a/contrib/fedora/rpm/readme-ifcfg-rh.txt b/contrib/fedora/rpm/readme-ifcfg-rh.txt
new file mode 100644
index 00000000..56c373d4
--- /dev/null
+++ b/contrib/fedora/rpm/readme-ifcfg-rh.txt
@@ -0,0 +1,63 @@
+NetworkManager stores new network profiles in keyfile format in the
+/etc/NetworkManager/system-connections/ directory.
+
+Previously, NetworkManager stored network profiles in ifcfg format
+in this directory (/etc/sysconfig/network-scripts/). However, the ifcfg
+format is deprecated. By default, NetworkManager no longer creates
+new profiles in this format.
+
+Connection profiles in keyfile format have many benefits. For example,
+this format is INI file-based and can easily be parsed and generated.
+
+Each section in NetworkManager keyfiles corresponds to a NetworkManager
+setting name as described in the nm-settings(5) and nm-settings-keyfile(5)
+man pages. Each key-value-pair in a section is one of the properties
+listed in the settings specification of the man page.
+
+If you still use network profiles in ifcfg format, consider migrating
+them to keyfile format. To migrate all profiles at once, enter:
+
+# nmcli connection migrate
+
+This command migrates all profiles from ifcfg format to keyfile
+format and stores them in /etc/NetworkManager/system-connections/.
+
+Alternatively, to migrate only a specific profile, enter:
+
+# nmcli connection migrate <profile_name|UUID|D-Bus_path>
+
+For further details, see:
+* nm-settings-keyfile(5)
+* nmcli(1)
+
+Interface renaming
+==================
+
+Connection profiles stored in ifcfg-rh format support the renaming of
+interfaces via udev. This is done via a helper tool
+/usr/lib/udev/rename_device that is invoked by udev to parse the files
+in /etc/sysconfig/network-scripts; when the HWADDR and DEVICE
+variables are set, the interface that matches the MAC address in
+HWADDR is renamed to the name specified in DEVICE.
+
+Connections in keyfile format don't provide the same integration with
+udev. The renaming of interfaces must be configured directly in udev,
+for example by creating a file:
+
+  /etc/systemd/network/70-rename.link
+
+with content:
+
+  [Match]
+  MACAddress=00:11:22:33:44:56
+
+  [Link]
+  Name=ethernet1
+
+Alternatively, a udev rule can also be used, such as:
+
+  /etc/udev/rules.d/70-interface-names.rules
+
+with content:
+
+  SUBSYSTEM=="net",ACTION=="add",ATTR{address}=="00:11:22:33:44:56",ATTR{type}=="1",NAME="ethernet1"
diff --git a/contrib/fedora/rpm/release.sh b/contrib/fedora/rpm/release.sh
new file mode 100755
index 00000000..809810f0
--- /dev/null
+++ b/contrib/fedora/rpm/release.sh
@@ -0,0 +1,604 @@
+#!/bin/bash
+
+# Script for doing NetworkManager releases.
+#
+# Run with --help for usage.
+#
+# There are 6 modes:
+#
+#  - "devel" : on main branch to tag a devel release (e.g. "1.25.2-dev").
+#  - "rc1"   : the first release candidate on "main" branch which branches off
+#              a new "nm-1-X" branch (e.g. tag "1.26-rc1" (1.25.90) and branch
+#              off "nm-1-26"). On main this also bumps the version number
+#              and creates a new devel release (e.g. "1.27.0-dev").
+#  - "rc"    : further release candidates on RC branch (e.g. from "nm-1-26" branch
+#              tag "1.26-rc2" with version number 1.25.91).
+#  - "major" : on stable branch do a major release (e.g. on "nm-1-26" branch
+#              release "1.26.0", followed by "1.26.1-dev").
+#              You should do a "major-post" release right a "major" release.
+#  - "major-post": after a "major" release, merge the release branch with main and
+#              do another devel snapshot on main (e.g. do "1.27.1-dev" release).
+#  - "minor" : on a stable branch do a minor release (e.g. "1.26.4" on "nm-1-26"
+#              branch and bump to "1.26.5-dev").
+#
+# Requisites:
+#
+#   * You need to start with a clean working directory (git clean -fdx)
+#
+#   * Run in a "clean" environment, i.e. no unusual environment variables set, on a recent
+#     Fedora, with suitable dependencies installed.
+#
+#   * First, ensure that you have ssh keys for "master.gnome.org" installed (and ssh-agent running).
+#     Also, ensure you have a GPG key that you want to use for signing. Also, have gpg-agent running
+#     and possibly configure `git config --get user.signingkey` for the proper key.
+#
+#   * Your git repository needs a remote "origin" that points to the upstream git repository
+#     (or set $ORIGIN) and use the standard git refs/remotes/$ORIGIN/ branch names.
+#
+#   * All your (relevant) local branches (main and nm-1-*) must be up to date with their
+#     remote tracking branches for origin.
+#
+# Run with --no-test to do the actual release.
+
+die() {
+    echo -n "FAIL: "
+    echo_color 31 "$@"
+    exit 1
+}
+
+echo_color() {
+    local color="$1"
+    shift
+    echo -e -n "\033[0;${color}m"
+    echo "$@"
+    echo -e -n '\033[0m'
+}
+
+print_usage() {
+    echo "Usage:"
+    echo "  $BASH_SOURCE [devel|rc1|rc|major|major-post|minor]"
+    echo "     [--no-test] \\"
+    echo "     [--no-find-backports] \\"
+    echo "     [--no-cleanup] \\"
+    echo "     [--allow-local-branches] \\"
+    echo "     [--no-check-gitlab] \\"
+    echo "     [--no-check-news] \\"
+    echo "     [--no-warn-publish-docs] \\"
+}
+
+die_help() {
+    print_usage
+    echo
+    sed -e '/^# /,/# Run with --no-test/!d' -e 's/^#\($\| \)/  /' "$BASH_SOURCE"
+    exit 0
+}
+
+die_usage() {
+    echo -n "FAIL: "
+    echo_color 31 "$@"
+    echo
+    print_usage
+    exit 1
+}
+
+do_command() {
+    local color=36
+    if [ "$DRY_RUN" = 0 ]; then
+        color=31
+    fi
+    echo -n "COMMAND: "
+    echo_color $color -n "$@"
+    echo
+    if [ "$DRY_RUN" = 0 ]; then
+        "$@"
+    fi
+}
+
+parse_version() {
+    local MAJ="$(sed -n '1,20 s/^m4_define(\[nm_major_version\], \[\([0-9]\+\)\])$/\1/p' ./configure.ac)"
+    local MIN="$(sed -n '1,20 s/^m4_define(\[nm_minor_version\], \[\([0-9]\+\)\])$/\1/p' ./configure.ac)"
+    local MIC="$(sed -n '1,20 s/^m4_define(\[nm_micro_version\], \[\([0-9]\+\)\])$/\1/p' ./configure.ac)"
+
+    re='^(0|[1-9][0-9]*) (0|[1-9][0-9]*) (0|[1-9][0-9]*)$'
+    [[ "$MAJ $MIN $MIC" =~ $re ]] || return 1
+    echo "$MAJ $MIN $MIC"
+}
+
+number_is_even() {
+    local re='^[0-9]*[02468]$'
+    [[ "$1" =~ $re ]]
+}
+
+number_is_odd() {
+    local re='^[0-9]*[13579]$'
+    [[ "$1" =~ $re ]]
+}
+
+git_same_ref() {
+    local a="$(git rev-parse "$1" 2>/dev/null)" || return 1
+    local b="$(git rev-parse "$2" 2>/dev/null)" || return 1
+    [ "$a" = "$b" ]
+}
+
+check_gitlab_pipeline() {
+    local BRANCH="$1"
+    local SHA="$2"
+    local PIPELINE_ID
+
+    PIPELINE_ID="$(curl --no-progress-meter "https://gitlab.freedesktop.org/api/v4/projects/411/pipelines?ref=$BRANCH&sha=$SHA&order_by=id" 2>/dev/null | jq '.[0].id')"
+    if ! [[ $PIPELINE_ID =~ [0-9]+ ]] ; then
+        echo "Cannot find pipeline for branch $BRANCH. Check \"https://gitlab.freedesktop.org/NetworkManager/NetworkManager/pipelines?page=1&scope=branches&ref=$BRANCH\""
+        return 1
+    fi
+
+    PIPELINE_STATUSES="$(curl --no-progress-meter "https://gitlab.freedesktop.org/api/v4/projects/411/pipelines/$PIPELINE_ID/jobs?per_page=100" 2>/dev/null | jq '.[] | select(.stage!="prep" and .stage!="tier3") | .status')"
+
+    if ! echo "$PIPELINE_STATUSES" | grep -q '^"success"$' ; then
+        echo "Cannot find successful jobs for branch $BRANCH. Check \"https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/pipelines/$PIPELINE_ID\""
+        return 1
+    fi
+    if echo "$PIPELINE_STATUSES" | grep -q -v '^"success"$' ; then
+        echo "Seems not all jobs for $BRANCH ran (or were successfull). Check \"https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/pipelines/$PIPELINE_ID\""
+        return 1
+    fi
+
+    return 0
+}
+
+set_version_number_autotools() {
+    sed -i \
+        -e '1,20 s/^m4_define(\[nm_major_version\], \[\([0-9]\+\)\])$/m4_define([nm_major_version], ['"$1"'])/' \
+        -e '1,20 s/^m4_define(\[nm_minor_version\], \[\([0-9]\+\)\])$/m4_define([nm_minor_version], ['"$2"'])/' \
+        -e '1,20 s/^m4_define(\[nm_micro_version\], \[\([0-9]\+\)\])$/m4_define([nm_micro_version], ['"$3"'])/' \
+        ./configure.ac
+}
+
+set_version_number_meson() {
+    sed -i \
+        -e '1,20 s/^\( *version: *'\''\)[0-9]\+\.[0-9]\+\.[0-9]\+\('\'',\)$/\1'"$1.$2.$3"'\2/' \
+        meson.build
+}
+
+set_version_number() {
+    set_version_number_autotools "$@" &&
+    set_version_number_meson "$@"
+}
+
+check_news() {
+    local mode="$1"
+    shift
+    local ver_arr=("$@")
+
+    case "$mode" in
+        major|minor)
+            if git grep -q 'NOT RECOMMENDED FOR PRODUCTION USE' -- ./NEWS ; then
+                return 1
+            fi
+            ;;
+        *)
+            ;;
+    esac
+    return 0
+}
+
+DO_CLEANUP=1
+CLEANUP_CHECKOUT_BRANCH=
+CLEANUP_REFS=()
+cleanup() {
+    if [ $DO_CLEANUP = 1 ]; then
+        [ -n "$CLEANUP_CHECKOUT_BRANCH" ] && git checkout -f "$CLEANUP_CHECKOUT_BRANCH"
+        for c in "${CLEANUP_REFS[@]}"; do
+            echo "delete reference. Restore with $(echo_color 36 -n git update-ref \"$c\" $(git rev-parse "$c"))"
+            git update-ref -d "$c"
+        done
+    fi
+}
+
+trap cleanup EXIT
+
+DIR="$(git rev-parse --show-toplevel)"
+
+ORIGIN=origin
+
+BASH_SOURCE_ABSOLUTE="$(readlink -f "$BASH_SOURCE")"
+
+test -d "$DIR" &&
+cd "$DIR" &&
+test -f ./contrib/fedora/rpm/build_clean.sh || die "cannot find NetworkManager base directory"
+
+RELEASE_MODE=""
+DRY_RUN=1
+FIND_BACKPORTS=1
+ALLOW_LOCAL_BRANCHES=0
+HELP_AND_EXIT=1
+CHECK_GITLAB=1
+WARN_PUBLISH_DOCS=1
+CHECK_NEWS=1
+while [ "$#" -ge 1 ]; do
+    A="$1"
+    shift
+    HELP_AND_EXIT=0
+    case "$A" in
+        --no-test)
+            DRY_RUN=0
+            ;;
+        --no-find-backports)
+            FIND_BACKPORTS=0
+            ;;
+        --no-cleanup)
+            DO_CLEANUP=0
+            ;;
+        --allow-local-branches)
+            # by default, the script errors out if the relevant branch (main, nm-1-Y) are not the same
+            # as the remote branch on origin. You should not do a release if you have local changes
+            # that differ from upstream. Set this flag to override that check.
+            ALLOW_LOCAL_BRANCHES=1
+            ;;
+        --no-check-gitlab)
+            CHECK_GITLAB=0
+            ;;
+        --no-warn-publish-docs)
+            WARN_PUBLISH_DOCS=0
+            ;;
+        --no-check-news)
+            CHECK_NEWS=0
+            ;;
+        --help|-h)
+            die_help
+            ;;
+        devel|rc1|rc|major|major-post|minor)
+            [ -z "$RELEASE_MODE" ] || die_usage "duplicate release-mode"
+            RELEASE_MODE="$A"
+            ;;
+        *)
+            die_usage "unknown argument \"$A\""
+            ;;
+    esac
+done
+[ "$HELP_AND_EXIT" = 1 ] && die_help
+
+[ -n "$RELEASE_MODE" ] || die_usage "specify the desired release mode"
+
+VERSION_ARR=( $(parse_version) ) || die "cannot detect NetworkManager version"
+VERSION_STR="$(IFS=.; echo "${VERSION_ARR[*]}")"
+
+echo "Current version before release: $VERSION_STR (do \"$RELEASE_MODE\" release)"
+
+grep -q "version: '${VERSION_ARR[0]}.${VERSION_ARR[1]}.${VERSION_ARR[2]}'," ./meson.build || die "meson.build does not have expected version"
+
+TMP="$(git status --porcelain)" || die "git status failed"
+test -z "$TMP" || die "git working directory is not clean (git status --porcelain)"
+
+TMP="$(LANG=C git clean -ndx)" || die "git clean -ndx failed"
+test -z "$TMP" || die "git working directory is not clean? (git clean -ndx)"
+
+CUR_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
+CUR_HEAD="$(git rev-parse HEAD)"
+TMP_BRANCH=release-branch
+
+if [ "$CUR_BRANCH" = main ]; then
+    number_is_odd "${VERSION_ARR[1]}" || die "Unexpected version number on main. Should be an odd development version"
+    [ "$RELEASE_MODE" = devel -o "$RELEASE_MODE" = rc1 -o "$RELEASE_MODE" = major-post ] || die "Unexpected branch name \"$CUR_BRANCH\" for \"$RELEASE_MODE\""
+else
+    re='^nm-[0-9]+-[0-9]+$'
+    [[ "$CUR_BRANCH" =~ $re ]] || die "Unexpected current branch $CUR_BRANCH. Should be main or nm-?-??"
+    if number_is_odd "${VERSION_ARR[1]}"; then
+        # we are on a release candiate branch.
+        [ "$RELEASE_MODE" = rc -o "$RELEASE_MODE" = major ] || die "Unexpected branch name \"$CUR_BRANCH\" for \"$RELEASE_MODE\""
+        [ "$CUR_BRANCH" == "nm-${VERSION_ARR[0]}-$((${VERSION_ARR[1]} + 1))" ] || die "Unexpected current branch $CUR_BRANCH. Should be nm-${VERSION_ARR[0]}-$((${VERSION_ARR[1]} + 1))"
+    else
+        [ "$RELEASE_MODE" = minor ] || die "Unexpected branch name \"$CUR_BRANCH\" for \"$RELEASE_MODE\""
+        [ "$CUR_BRANCH" == "nm-${VERSION_ARR[0]}-${VERSION_ARR[1]}" ] || die "Unexpected current branch $CUR_BRANCH. Should be nm-${VERSION_ARR[0]}-${VERSION_ARR[1]}"
+    fi
+fi
+
+RC_VERSION=
+RELEASE_BRANCH=
+case "$RELEASE_MODE" in
+    minor)
+        number_is_even "${VERSION_ARR[1]}" &&
+        number_is_odd  "${VERSION_ARR[2]}" || die "cannot do minor release on top of version $VERSION_STR"
+        [ "$CUR_BRANCH" != main ] || die "cannot do a minor release on main"
+        ;;
+    devel)
+        number_is_odd "${VERSION_ARR[1]}" || die "cannot do devel release on top of version $VERSION_STR"
+        [ "$((${VERSION_ARR[2]} + 1))" -lt 90 ] || die "devel release must have a micro version smaller than 90 but current version is $VERSION_STR"
+        [ "$CUR_BRANCH" == main ] || die "devel release can only be on main"
+        ;;
+    rc)
+        number_is_odd "${VERSION_ARR[1]}" || die "cannot do rc release on top of version $VERSION_STR"
+        [ "${VERSION_ARR[2]}" -ge 90 ] || die "rc release must have a micro version larger than ${VERSION_ARR[0]}.90 but current version is $VERSION_STR"
+        RC_VERSION="$((${VERSION_ARR[2]} - 88))"
+        [ "$CUR_BRANCH" == "nm-${VERSION_ARR[0]}-$((${VERSION_ARR[1]} + 1))" ] || die "devel release can only be on \"nm-${VERSION_ARR[0]}-$((${VERSION_ARR[1]} + 1))\" branch"
+        ;;
+    rc1)
+        number_is_odd "${VERSION_ARR[1]}" || die "cannot do rc release on top of version $VERSION_STR"
+        [ "${VERSION_ARR[2]}" -lt 90 ] || die "rc release must have a micro version smaller than ${VERSION_ARR[0]}.${VERSION_ARR[1]}.90 but current version is $VERSION_STR"
+        [ "$CUR_BRANCH" == main ] || die "rc1 release can only be on main"
+        RELEASE_BRANCH="nm-${VERSION_ARR[0]}-$((${VERSION_ARR[1]} + 1))"
+        ;;
+    major)
+        number_is_odd "${VERSION_ARR[1]}" || die "cannot do major release on top of version $VERSION_STR"
+        [ "${VERSION_ARR[2]}" -ge 90 ] || die "parent version for major release must have a micro version larger than ${VERSION_ARR[0]}.90 but current version is $VERSION_STR"
+        [ "$CUR_BRANCH" == "nm-${VERSION_ARR[0]}-$((${VERSION_ARR[1]} + 1))" ] || die "major release can only be on \"nm-${VERSION_ARR[0]}-$((${VERSION_ARR[1]} + 1))\" branch"
+        ;;
+    major-post)
+        number_is_odd "${VERSION_ARR[1]}" || die "cannot do major-post release on top of version $VERSION_STR"
+        [ "$((${VERSION_ARR[2]} + 1))" -lt 90 ] || die "major-post release must have a micro version smaller than 90 but current version is $VERSION_STR"
+        [ "$CUR_BRANCH" == main ] || die "major-post release can only be on main"
+        ;;
+    *)
+        die "Release mode $RELEASE_MODE not yet implemented"
+        ;;
+esac
+
+git fetch "$ORIGIN" || die "git fetch failed"
+
+if [ "$ALLOW_LOCAL_BRANCHES" != 1 ]; then
+    git_same_ref "$CUR_BRANCH" "refs/heads/$CUR_BRANCH" || die "Current branch $CUR_BRANCH is not a branch??"
+    git_same_ref "$CUR_BRANCH" "refs/remotes/$ORIGIN/$CUR_BRANCH" || die "Current branch $CUR_BRANCH seems not up to date with refs/remotes/$ORIGIN/$CUR_BRANCH. Git pull or --allow-local-branches?"
+fi
+
+NEWER_BRANCHES=()
+if [ "$CUR_BRANCH" != main ]; then
+    i="${VERSION_ARR[1]}"
+    while : ; do
+        i=$((i + 2))
+        b="nm-${VERSION_ARR[0]}-$i"
+        if ! git show-ref --verify --quiet "refs/remotes/$ORIGIN/$b"; then
+            git show-ref --verify --quiet "refs/heads/$b" && die "unexpectedly branch $b exists"
+            break
+        fi
+        if [ "$ALLOW_LOCAL_BRANCHES" != 1 ]; then
+            git_same_ref "$b" "refs/heads/$b" || die "branch $b is not a branch??"
+            git_same_ref "$b" "refs/remotes/$ORIGIN/$b" || die "branch $b seems not up to date with refs/remotes/$ORIGIN/$b. Git pull or --allow-local-branches?"
+        fi
+        NEWER_BRANCHES+=("refs/heads/$b")
+    done
+    b=main
+    if [ "$ALLOW_LOCAL_BRANCHES" != 1 ]; then
+        git_same_ref "$b" "refs/heads/$b" || die "branch $b is not a branch??"
+        git_same_ref "$b" "refs/remotes/$ORIGIN/$b" || die "branch $b seems not up to date with refs/remotes/$ORIGIN/$b. Git pull or --allow-local-branches?"
+    fi
+fi
+
+if [ -n "$RELEASE_BRANCH" ]; then
+    git show-ref --verify --quiet "refs/remotes/$ORIGIN/$RELEASE_BRANCH" && die "release branch refs/remotes/$ORIGIN/$RELEASE_BRANCH unexpectedly exists already"
+    git show-ref --verify --quiet "refs/heads/$RELEASE_BRANCH" && die "release branch refs/heads/$RELEASE_BRANCH unexpectedly exists already"
+fi
+
+if [ "$ALLOW_LOCAL_BRANCHES" != 1 ]; then
+    cmp <(git show "$ORIGIN/main:contrib/fedora/rpm/release.sh") "$BASH_SOURCE_ABSOLUTE" || die "$BASH_SOURCE is not identical to \`git show \"$ORIGIN/main:contrib/fedora/rpm/release.sh\"\`"
+fi
+
+if ! check_news "$RELEASE_MODE" "@{VERSION_ARR[@]}" ; then
+    if [ "$CHECK_NEWS" == 1 ]; then
+        die "NEWS file needs update to mention stable release (skip check with --no-check-news)"
+    fi
+    echo "WARNING: NEWS file needs update to mention stable release (test skipped with --no-check-news)"
+fi
+
+if [ "$RELEASE_MODE" = major -o "$RELEASE_MODE" = minor ]; then
+    echo
+    latest=
+    if [ "$RELEASE_MODE" = major ]; then
+        echo "Note that after the new major you have to publish the new documentation on"
+        latest=" -l"
+    else
+        echo "Note that after the stable release you maybe should publish the new documentation on"
+        latest=" [-l]"
+    fi
+    echo "$(echo_color 36 -n "https://gitlab.freedesktop.org/NetworkManager/networkmanager.pages.freedesktop.org.git") by running"
+    if [ "$RELEASE_MODE" = major ]; then
+        v="${VERSION_ARR[0]}.$((${VERSION_ARR[1]} + 1)).0"
+    else
+        v="${VERSION_ARR[0]}.${VERSION_ARR[1]}.$((${VERSION_ARR[2]} + 1))"
+    fi
+    echo "  \`$(echo_color 36 -n "./scripts/import-docs.sh $v$latest")\`"
+    echo
+    if [ $WARN_PUBLISH_DOCS = 1 ]; then
+        echo "Avoid this prompt via \"--no-warn-publish-docs\""
+        read -p "Please confirm that you know [ENTER] "
+    fi
+fi
+
+if [ $FIND_BACKPORTS = 1 ]; then
+    git show "$ORIGIN/main:contrib/scripts/find-backports" > ./.git/nm-find-backports \
+    && chmod +x ./.git/nm-find-backports \
+    || die "cannot get contrib/scripts/find-backports"
+
+    TMP="$(./.git/nm-find-backports "$CUR_BRANCH" main "${NEWER_BRANCHES[@]}" 2>/dev/null)" || die "nm-find-backports failed"
+    test -z "$TMP" || die "nm-find-backports returned patches that need to be backported (ignore with --no-find-backports): ./.git/nm-find-backports \"$CUR_BRANCH\" main ${NEWER_BRANCHES[@]}"
+fi
+
+if [ $CHECK_GITLAB = 1 ]; then
+    if ! check_gitlab_pipeline "$CUR_BRANCH" "$CUR_HEAD" ; then
+        echo "Check the pipelines for branch \"$CUR_BRANCH\" at https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/pipelines?ref=$CUR_BRANCH"
+        echo "Wait for pipeline with \`ci-fairy wait-for-pipeline --project NetworkManager/NetworkManager --sha \"$CUR_HEAD\"\`"
+        die "It seems not all gitlab-ci jobs were running/succeeding. Skip this check with --no-check-gitlab"
+    fi
+fi
+
+BRANCHES=()
+BUILD_TAG=
+
+CLEANUP_CHECKOUT_BRANCH="$CUR_BRANCH"
+
+git checkout -B "$TMP_BRANCH"
+CLEANUP_REFS+=("refs/heads/$TMP_BRANCH")
+
+case "$RELEASE_MODE" in
+    minor)
+        set_version_number "${VERSION_ARR[0]}" "${VERSION_ARR[1]}" $(("${VERSION_ARR[2]}" + 1))
+        git commit -m "release: bump version to ${VERSION_ARR[0]}.${VERSION_ARR[1]}.$(("${VERSION_ARR[2]}" + 1))" -a || die "failed to commit release"
+        set_version_number "${VERSION_ARR[0]}" "${VERSION_ARR[1]}" $(("${VERSION_ARR[2]}" + 2))
+        git commit -m "release: bump version to ${VERSION_ARR[0]}.${VERSION_ARR[1]}.$(("${VERSION_ARR[2]}" + 2)) (development)" -a || die "failed to commit devel version bump"
+
+        b="${VERSION_ARR[0]}.${VERSION_ARR[1]}.$(("${VERSION_ARR[2]}" + 1))"
+        git tag -s -a -m "Tag $b" "$b" HEAD~ || die "failed to tag release"
+        BRANCHES+=("$b")
+        CLEANUP_REFS+=("refs/tags/$b")
+        BUILD_TAG="$b"
+        b="${VERSION_ARR[0]}.${VERSION_ARR[1]}.$(("${VERSION_ARR[2]}" + 2))"
+        git tag -s -a -m "Tag $b (development)" "$b-dev" HEAD || die "failed to tag devel version"
+        BRANCHES+=("$b-dev")
+        CLEANUP_REFS+=("refs/tags/$b-dev")
+        TAR_VERSION="$BUILD_TAG"
+        ;;
+    devel)
+        set_version_number "${VERSION_ARR[0]}" "${VERSION_ARR[1]}" $(("${VERSION_ARR[2]}" + 1))
+        git commit -m "release: bump version to ${VERSION_ARR[0]}.${VERSION_ARR[1]}.$(("${VERSION_ARR[2]}" + 1)) (development)" -a || die "failed to commit devel version bump"
+
+        b="${VERSION_ARR[0]}.${VERSION_ARR[1]}.$(("${VERSION_ARR[2]}" + 1))"
+        git tag -s -a -m "Tag $b (development)" "$b-dev" HEAD || die "failed to tag release"
+        BRANCHES+=("$b-dev")
+        CLEANUP_REFS+=("refs/tags/$b-dev")
+        BUILD_TAG="$b-dev"
+        TAR_VERSION="$b"
+        ;;
+    rc)
+        b="${VERSION_ARR[0]}.${VERSION_ARR[1]}.$(("${VERSION_ARR[2]}" + 1))"
+        t="${VERSION_ARR[0]}.$(("${VERSION_ARR[1]}" + 1))-rc$RC_VERSION"
+        set_version_number "${VERSION_ARR[0]}" "${VERSION_ARR[1]}" $(("${VERSION_ARR[2]}" + 1))
+        git commit -m "release: bump version to $b ($t) (development)" -a || die "failed to commit rc version bump"
+
+        git tag -s -a -m "Tag $b ($t) (development)" "$t" HEAD || die "failed to tag release"
+        BRANCHES+=("$t")
+        CLEANUP_REFS+=("refs/tags/$t")
+        BUILD_TAG="$t"
+        TAR_VERSION="$b"
+        ;;
+    rc1)
+        set_version_number "${VERSION_ARR[0]}" "${VERSION_ARR[1]}" 90
+        b="${VERSION_ARR[0]}.${VERSION_ARR[1]}.90"
+        t="${VERSION_ARR[0]}.$(("${VERSION_ARR[1]}" + 1))-rc1"
+        git commit -m "release: bump version to $b ($t)" -a || die "failed to commit rc1 version bump"
+
+        git tag -s -a -m "Tag $b ($t) (development)" "$t" HEAD || die "failed to tag release $t"
+        BRANCHES+=("$t")
+        CLEANUP_REFS+=("refs/tags/$t")
+        BUILD_TAG="$t"
+        TAR_VERSION="$b"
+        ;;
+    major)
+        b="${VERSION_ARR[0]}.$((${VERSION_ARR[1]} + 1)).0"
+        b2="${VERSION_ARR[0]}.$((${VERSION_ARR[1]} + 1)).1"
+
+        set_version_number "${VERSION_ARR[0]}" "$((${VERSION_ARR[1]} + 1))" 0
+        git commit -m "release: bump version to $b" -a || die "failed to commit major version bump"
+        git tag -s -a -m "Tag $b" "$b" HEAD || die "failed to tag release"
+        BRANCHES+=("$b")
+        CLEANUP_REFS+=("refs/tags/$b")
+
+        set_version_number "${VERSION_ARR[0]}" "$((${VERSION_ARR[1]} + 1))" 1
+        git commit -m "release: bump version to $b2 (development)" -a || die "failed to commit another bump after major version bump"
+        git tag -s -a -m "Tag $b (development)" "$b2-dev" HEAD || die "failed to tag release"
+        BRANCHES+=("$b2-dev")
+        CLEANUP_REFS+=("refs/tags/$b2-dev")
+
+        BUILD_TAG="$b"
+        TAR_VERSION="$b"
+        ;;
+    major-post)
+        # We create a merge commit with the content of current "main", with two
+        # parent commits $THE_RELEASE and "main". But we want that the first parent
+        # is the release, so that `git log --first-parent` follows the path with the
+        # release candidates, and not the devel part during that time. Hence this
+        # switcheroo here.
+        git checkout -B "$TMP_BRANCH" "${VERSION_ARR[0]}.$((${VERSION_ARR[1]} - 1)).0" || die "merge0"
+        git merge -Xours --commit -m tmp main || die "merge1"
+        git rm --cached -r . || die "merge2"
+        git checkout main -- . || die "merge3"
+        b="${VERSION_ARR[0]}.${VERSION_ARR[1]}.$((${VERSION_ARR[2]} + 1))"
+        git commit --amend -m tmp -a || die "failed to commit major version bump"
+        test x = "x$(git diff main HEAD)" || die "there is a diff after merge!"
+
+        set_version_number "${VERSION_ARR[0]}" "${VERSION_ARR[1]}" "$((${VERSION_ARR[2]} + 1))"
+        git commit --amend -m "release: bump version to $b (development)" -a || die "failed to commit major version bump"
+        git tag -s -a -m "Tag $b (development)" "$b-dev" HEAD || die "failed to tag release"
+        BRANCHES+=("$b-dev")
+        CLEANUP_REFS+=("refs/tags/$b-dev")
+        BUILD_TAG="$b-dev"
+        TAR_VERSION="$b"
+        ;;
+    *)
+        die "Release mode $RELEASE_MODE not yet implemented"
+        ;;
+esac
+
+build_tag() {
+    git checkout "$BUILD_TAG" || die "failed to checkout $BUILD_TAG"
+
+    ./contrib/fedora/rpm/build_clean.sh -r || die "build release failed"
+
+    test -f "./build/meson-dist/$RELEASE_FILE" \
+    || die "release file \"./build/meson-dist/$RELEASE_FILE\" not found"
+
+    cp "./build/meson-dist/$RELEASE_FILE" /tmp/ || die "failed to copy release tarball to /tmp"
+
+    if test -f "./build/meson-dist/$RELEASE_FILE.sig" ; then
+        cp "./build/meson-dist/$RELEASE_FILE.sig" /tmp/ || die "failed to copy signature for tarball to /tmp"
+    fi
+
+    git clean -fdx
+}
+
+RELEASE_FILES=()
+if [ -n "$BUILD_TAG" ]; then
+    RELEASE_FILE="NetworkManager-$TAR_VERSION.tar.xz"
+    RELEASE_FILES+=("$RELEASE_FILE")
+    build_tag
+fi
+git checkout -B "$CUR_BRANCH" "$TMP_BRANCH" || die "cannot checkout $CUR_BRANCH"
+
+BRANCHES+=( "$CUR_BRANCH" )
+
+if [ "$RELEASE_MODE" = rc1 ]; then
+    git branch "$RELEASE_BRANCH" "$TMP_BRANCH" || die "cannot checkout $CUR_BRANCH"
+    BRANCHES+=( "$RELEASE_BRANCH" )
+    CLEANUP_REFS+=( "refs/heads/$RELEASE_BRANCH" )
+fi
+
+if [ "$RELEASE_MODE" = rc1 ]; then
+    git checkout "$TMP_BRANCH"
+    b="${VERSION_ARR[0]}.$((${VERSION_ARR[1]} + 2)).0"
+    set_version_number "${VERSION_ARR[0]}" "$((${VERSION_ARR[1]} + 2))" 0
+    git commit -m "release: bump version to $b (development)" -a || die "failed to commit devel version bump"
+    git tag -s -a -m "Tag $b (development)" "$b-dev" HEAD || die "failed to tag release"
+    BRANCHES+=("$b-dev")
+    CLEANUP_REFS+=("refs/tags/$b-dev")
+    BUILD_TAG="$b-dev"
+    TAR_VERSION="$b"
+    RELEASE_FILE="NetworkManager-$TAR_VERSION.tar.xz"
+    RELEASE_FILES+=("$RELEASE_FILE")
+    build_tag
+    git checkout -B "$CUR_BRANCH" "$TMP_BRANCH" || die "cannot checkout $CUR_BRANCH"
+fi
+
+if ! [ "$DRY_RUN" = 0 ]; then
+    ssh master.gnome.org true || die "failed to \`ssh master.gnome.org\`"
+fi
+
+for r in "${RELEASE_FILES[@]}"; do
+    do_command rsync -va --append-verify -P "/tmp/$r" master.gnome.org: || die "failed to rsync \"/tmp/$r\""
+done
+
+do_command git push "$ORIGIN" "${BRANCHES[@]}" || die "failed to to push branches ${BRANCHES[@]} to $ORIGIN"
+
+FAIL=0
+for r in "${RELEASE_FILES[@]}"; do
+    do_command ssh master.gnome.org ftpadmin install --unattended "$r" || FAIL=1
+done
+if [ "$FAIL" = 1 ]; then
+    die "ftpadmin install failed. This was the last step. Invoke the command manually"
+fi
+
+CLEANUP_CHECKOUT_BRANCH=
+if [ "$DRY_RUN" = 0 ]; then
+    CLEANUP_REFS=()
+    git branch -D "$TMP_BRANCH"
+else
+    H="$(git rev-parse "$CUR_BRANCH")"
+    git checkout -B "$CUR_BRANCH" "$CUR_HEAD" || die "cannot reset $CUR_BRANCH to $CUR_HEAD"
+    echo "delete reference. Restore with $(echo_color 36 -n git checkout -B "\"$CUR_BRANCH\"" "$H")"
+fi
diff --git a/contrib/fedora/utils/makerepo.sh b/contrib/fedora/utils/makerepo.sh
new file mode 100755
index 00000000..266aac8f
--- /dev/null
+++ b/contrib/fedora/utils/makerepo.sh
@@ -0,0 +1,698 @@
+#!/bin/bash
+
+#
+# The script is ugly but is here to help to create a git-repository
+# based on dist-git.
+#
+#  * Works with fedpkg and rhpkg
+#  * Different packages are supported. See detect_build_type below.
+#  * Creates first an initial commit of the source directory (after "fedpkg prep")
+#  * Excludes files and creates a gitignore file. It does so by .git/makerepo.gitignore
+#    which can be edited manually. Also, after a `$0 local`, it will record all files
+#    with modifications to be ignored in the future.
+#  * Revert each patch from the spec file
+#  * Reapply each patch until you are where were originally (sans ignored files)
+#  * Restore again the original state, i.e. replying the patches (including ignored
+#    files -- that are no longer part of master-tip).
+#  * Fetch from upstream origin (and add as remote)
+#  * Fetch from a local git repository (and add as remote)
+#  * It can detect the parent commit where the package branched of
+#    and rebase the created history on top of that.
+#  * optionally, do `fedpkg local`.
+#
+# ONE-TIME SETUP:
+#   - clone the dist-git package
+#       $ PACKAGE=libnl3
+#       $ fedpkg clone $PACKAGE
+#       $ cd $PACKAGE
+#
+#   - configure local git-repository (optional)
+#       $ ln -s /path/to/local/clone .git/local
+#
+#   - create initial gitignore file (optional)
+#       $ edit .git/makerepo.gitignore
+#     or
+#       $ edit .git/makerepo.gitignore.$BRANCHNAME
+#
+# USAGE:
+#       $ cd $PACKAGE
+#       $ makerepo.sh
+#       $ makerepo.sh local
+#
+
+
+#set -vx
+
+die() {
+	echo "$@" >&2
+	exit 1
+}
+
+containsElement () {
+    local e
+    local name="$1"
+    shift
+    local i=0
+
+    for e in "${@:2}"; do
+        if [[ "$e" == "$1" ]]; then
+            eval "$name=$i"
+            return 0;
+        fi
+        i=$((i+1))
+    done
+    return 1
+}
+
+git_remote_add_gnome() {
+    git remote add "${2-origin}" "https://gitlab.gnome.org/GNOME/$1.git" && \
+    git remote 'set-url' --push "${2-origin}" "git@gitlab.gnome.org:GNOME/$1.git"
+}
+
+git_remote_add_github() {
+    git remote add "${2-origin}" "https://github.com/$1.git"
+    git remote 'set-url' --push "${2-origin}" "git@github.com:$1.git"
+}
+
+srcdir="$(readlink -f "$(git rev-parse --show-toplevel 2>/dev/null)")"
+[[ "x$srcdir" != x ]] || die "Could not detect dist-git directory (are you inside the git working directory?)"
+cd "$srcdir" || die "Could not switch to dist-git directory"
+
+
+if [[ "x$(ls -1d ./*.spec 2>/dev/null)" == x || ! -f "./sources" ]]; then
+    die "**Error**: Directory "\`$srcdir\'" does not look like the dist-git pkg dir."
+fi
+
+if [[ "$FEDPKG" == "" ]]; then
+    REMOTE="$(git config --get "branch.$(git branch --show-current).remote" 2>/dev/null)"
+    URL="$(git config --get "remote.$REMOTE.url")"
+    if [[ "$URL" = *'pkgs.devel.redhat.com'* ]]; then
+        FEDPKG=rhpkg
+    elif [[ "$URL" = *'gitlab.com'*'redhat/centos-stream'* ]]; then
+        FEDPKG=centpkg
+    elif [[ "$URL" = *'pkgs.fedoraproject.org/'* || "$URL" = *'src.fedoraproject.org/'* ]]; then
+        FEDPKG=fedpkg
+    else
+        die "not inside dist-git repository? Check out a branch that has the dist-git remote tracking branch >>$PWD<<"
+    fi
+fi
+
+split_patch() {
+    # patches created with git-format-patch that contain more then one
+    # commit, cannot be easily reverted with patch, because patch works
+    # the patches from top down. In case of -R however, we have to apply
+    # the latest patches first.
+
+    read -r -d '' PERL_PROG <<-'EOF'
+		use strict;
+		use warnings;
+
+		open FILE, $ARGV[0]  or die "Can't open $ARGV[0] for reading: $!\n";
+
+		local $/ = undef;
+		my $file = <FILE>;
+		close FILE;
+
+		my @patches = split(/\n\nFrom /,$file);
+
+		my $i = $#patches + 1;
+		my $patch;
+		my $first = 1;
+		foreach $patch (@patches){
+			if ($first) {
+				$first = 0;
+			} else {
+				$patch = "From $patch"
+			}
+			my $o = sprintf("%s%s%03d", $ARGV[0], $ARGV[1], $i);
+			open(my $OUT, ">", $o) or die "Can't open $o for writing: $!";
+			$i--;
+
+			print $OUT "$patch";
+
+			close $OUT;
+		}
+	EOF
+
+    perl -e "$PERL_PROG" "$1" "$2"
+}
+
+spec_parse_patch_p() {
+    local SPEC="$1"
+    local NUM="$2"
+
+    local P="$(sed -n "s/^%\<patch$NUM\>.* -p\([0-9]\+\) .*$/\1/p" "$SPEC")"
+
+    echo "${P:-1}"
+}
+
+get_patch_origin() {
+    local PATCH="$1"
+
+    (
+        cd "$srcdir"
+
+        local HASH="$(git log -n1 --format="%H" HEAD -- "$PATCH")"
+
+        if [[ "$HASH" == "" ]]; then
+            return
+        fi
+
+        printf "\n\nPatch \"%s\" was last modified in commit:\n\n" "$PATCH"
+        git log -n1 "$HASH" | sed 's/^[^ ]/    \0/'
+    )
+}
+
+print_synopsis() {
+    echo "SYNOPSIS: $(basename "$0") [--dist|-d DIST] [local|--local|-l] [-?|-h|--help|help] [NUM]"
+    echo "  - If [NUM] is omitted, it will revert all patches from the spec file,"
+    echo "    otherwise only the last NUM patches."
+    echo "  - When specifying 'local', it will also call \`$FEDPKG local\` to configure"
+    echo "    and build the output directory."
+    echo "  - '--dist' implies '--local'. This argument is passed to ${FEDPKG}."
+    echo "  TIP: symlink your local git clone of upstream to './.git/local'."
+}
+
+unset REVERT_COUNT
+LOCAL=0
+DIST=""
+while [ $# -ne 0 ]; do
+    ARG="$1"
+    shift
+    case "$ARG" in
+        -h|'-?'|help|--help)
+            print_synopsis
+            exit 0
+            ;;
+        local|--local|-l)
+            LOCAL=1
+            ;;
+        --dist|-d)
+            DIST="$1"
+            shift
+            if [ "x$DIST" = x ]; then
+                print_synopsis
+                die "--dist needs an argument"
+            fi
+            ;;
+        *)
+            if [ -n "${REVERT_COUNT+x}" ]; then
+                print_synopsis
+                die "invalid argument \"$ARG\""
+            fi
+            case "$ARG" in
+                ''|*[!0-9]*)
+                    print_synopsis
+                    die "invalid argument \"$ARG\": should be an integer (number of patches to revert)"
+                    ;;
+            esac
+            REVERT_COUNT="$ARG"
+            ;;
+    esac
+done
+
+if [ "x$DIST" != x ]; then
+    DIST=" --dist $DIST"
+fi
+
+# generate the clean dir
+$FEDPKG $DIST prep || die "error while \`$FEDPKG$DIST prep\`"
+
+detect_build_type() {
+    local TEST_DIR="./$1/"
+    local TEST_SPEC="$2"
+    local TEST_BUILD_TYPE="$3"
+    local DIRNAME
+
+    if [[ -n "$BUILD_TYPE" || -z "$1" || "x$(ls -1d $TEST_DIR 2>/dev/null)" == x || ! -f "$TEST_SPEC" ]]; then
+        return 1
+    fi
+
+    DIRNAME="$(ls -1d $TEST_DIR)" || die "could not find directory"
+    DIRNAME="$(basename "$DIRNAME")"
+    SPEC="$TEST_SPEC"
+
+    if [[ -n "$TEST_BUILD_TYPE" ]]; then
+        BUILD_TYPE="$TEST_BUILD_TYPE"
+    else
+        BUILD_TYPE="${TEST_SPEC%.spec}"
+    fi
+}
+
+detect_dirname() {
+    local BUILD_TYPE="$1"
+    local DIRS=()
+    local SOURCES
+    local D suffix T
+
+    SOURCES="$(sed 's/^\(SHA512 (\(.*\)) = [0-9a-f]\{128\}\|\([0-9a-f]\{32\} \+\(.*\)\)\)$/\2\4/' ./sources 2>/dev/null)"
+
+    for suffix in .tar.gz .tar.bz .tar.xz .tgz .tar.bz2 ; do
+        for T in ${SOURCES[@]}; do
+            if [[ "$T" == *$suffix ]]; then
+                D="${T%$suffix}"
+                [[ -d "$D" ]] && DIRS=("${DIRS[@]}" "$D")
+                D="$(tar -tf "$T" | sed 's#/.*##' | sort | uniq)"
+                [[ -d "$D" ]] && DIRS=("${DIRS[@]}" "$D")
+            fi
+        done
+
+        # iterate over all tarballs that start with "$BUILD_TYPE" and
+        # see if there exists a directory with the same name as
+        # the unpacked tarball (that is, stripping the suffix).
+        for T in $(ls -1 "$BUILD_TYPE"*"$suffix" 2>/dev/null); do
+            D="${T%$suffix}"
+            [[ -d "$D" ]] && DIRS=("${DIRS[@]}" "$D")
+        done
+    done
+
+    D=
+    if [[ ${#DIRS[@]} -ge 1 ]]; then
+        # return the newest directory.
+        D="$(ls -1d --sort=time --time=ctime "${DIRS[@]}" 2>/dev/null | head -n1)"
+    fi
+    if [[ "$D" != "" ]]; then
+        printf "%s" "$D"
+        return 0
+    fi
+    return 1
+}
+
+BUILD_TYPE=
+detect_build_type 'NetworkManager-[0-9]*' NetworkManager.spec
+detect_build_type 'network-manager-applet-[0-9]*' network-manager-applet.spec
+detect_build_type 'libnl-[0-9]*' libnl3.spec
+detect_build_type 'NetworkManager-openvpn-[0-9]*' NetworkManager-openvpn.spec
+detect_build_type 'NetworkManager-openswan-[0-9]*' NetworkManager-openswan.spec
+detect_build_type 'NetworkManager-libreswan-[0-9]*' NetworkManager-libreswan.spec
+detect_build_type 'NetworkManager-vpnc-[0-9]*' NetworkManager-vpnc.spec
+detect_build_type 'ModemManager-[0-9]*' ModemManager.spec
+detect_build_type 'wireless_tools.[0-9]*' wireless-tools.spec
+detect_build_type 'umip-[0-9]*' mipv6-daemon.spec
+detect_build_type 'initscripts-[0-9]*' initscripts.spec
+detect_build_type 'libqmi-[0-9]*' libqmi.spec
+detect_build_type 'libibverbs-[0-9]*' libibverbs.spec
+detect_build_type 'iproute2-*' iproute.spec
+detect_build_type 'glib-2*' glib2.spec
+detect_build_type 'vpnc-*' vpnc.spec
+detect_build_type 'gnome-control-center-*' control-center.spec gnome-control-center
+
+if [[ -z "$BUILD_TYPE" ]]; then
+    SPEC="$(ls -1 *.spec 2>/dev/null | head -n1)"
+    BUILD_TYPE="${SPEC%.spec}"
+    [[ -n "$BUILD_TYPE" ]] || die "Failed to detect repository type (no spec file)"
+
+    [[ -f sources ]] || die "Failed to detect repository type (no sources file)"
+fi
+
+DIRNAME="$(detect_dirname "$BUILD_TYPE")" || die "Failed to detect repository type (no directory)."
+
+CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)"
+if [[ "x$CURRENT_BRANCH" != x && -f "./.git/makerepo.gitignore-$CURRENT_BRANCH" ]]; then
+    MAKEREPO_GIT_IGNORE_MY="makerepo.gitignore-$CURRENT_BRANCH"
+elif [[ -f ./.git/makerepo.gitignore ]]; then
+    MAKEREPO_GIT_IGNORE_MY=makerepo.gitignore
+else
+    MAKEREPO_GIT_IGNORE_MY=""
+fi
+MAKEREPO_GIT_IGNORE_LAST="makerepo.gitignore.last-$CURRENT_BRANCH"
+
+get_local_mirror() {
+    local URL="$1"
+    local DIRNAME
+    local FULLNAME
+
+    if [[ -z "$URL" ]]; then
+        return
+    fi
+
+    [[ -n "$NO_REMOTE" ]] && return
+
+    DIRNAME="${URL##*/}"
+    DIRNAME="${DIRNAME%.git}"
+    FULLNAME="$srcdir/.git/.makerepo-${DIRNAME}.git"
+
+    if [ ! -d "$FULLNAME" ] && [ -d "$FULLNAME.git" ]; then
+        # due to a bug, old versions of the script might have created "*.git.git/" directories.
+        # rename.
+        mv "$FULLNAME.git" "$FULLNAME"
+    fi
+
+    if [[ ! -d "$FULLNAME" ]]; then
+        if [[ -f "$FULLNAME" ]]; then
+            # create a file with name $FULLNAME, to suppress local mirroring
+            return
+        fi
+        git clone --mirror --bare "$URL" "$FULLNAME/"
+    fi
+    (
+        cd "$FULLNAME"
+        git fetch origin --prune
+        git gc
+    )
+    echo "$FULLNAME"
+}
+
+pushd "$DIRNAME"
+    git init .
+    # if you have a local clone of upstream, symlink it as ../.git/local.
+    if [[ "$BUILD_TYPE" == "NetworkManager" ]]; then
+        git remote add origin "https://gitlab.freedesktop.org/NetworkManager/NetworkManager.git"
+        git remote 'set-url' --push origin "git@gitlab.freedesktop.org:NetworkManager/NetworkManager.git"
+        git config notes.displayRef refs/notes/bugs
+        git config --add remote.origin.fetch refs/tags/*:refs/tags/*
+        git config --add remote.origin.fetch refs/notes/bugs:refs/notes/bugs
+    elif [[ "$BUILD_TYPE" == "ModemManager" ]]; then
+        git remote add origin "git://anongit.freedesktop.org/ModemManager/ModemManager"
+        git remote 'set-url' --push origin "ssh://$USER@git.freedesktop.org/git/ModemManager/ModemManager"
+        git config --add remote.origin.fetch refs/tags/*:refs/tags/*
+    elif [[ "$BUILD_TYPE" == "libnl3" ]]; then
+        git_remote_add_github thom311/libnl
+    elif [[ "$BUILD_TYPE" == "network-manager-applet" ||
+            "$BUILD_TYPE" == "gnome-control-center" ||
+            "$BUILD_TYPE" == "libnma" ||
+            "$BUILD_TYPE" == "NetworkManager-fortisslvpn" ||
+            "$BUILD_TYPE" == "NetworkManager-libreswan" ||
+            "$BUILD_TYPE" == "NetworkManager-openconnect" ||
+            "$BUILD_TYPE" == "NetworkManager-openvpn" ||
+            "$BUILD_TYPE" == "NetworkManager-pptp" ||
+            "$BUILD_TYPE" == "NetworkManager-vpnc" ]]; then
+        git_remote_add_gnome "$BUILD_TYPE"
+    elif [[ "$BUILD_TYPE" == "glib2" ]]; then
+        git_remote_add_gnome glib
+    elif [[ "$BUILD_TYPE" == "NetworkManager-openswan" ]]; then
+        git remote add origin "git://git.gnome.org/network-manager-openswan";
+        git remote 'set-url' --push origin "ssh://$USER@git.gnome.org/git/network-manager-openswan"
+    elif [[ "$BUILD_TYPE" == "wpa_supplicant" ]]; then
+        git remote add origin "git://w1.fi/hostap.git"
+        git_remote_add_github NetworkManager/hostap nm
+    elif [[ "$BUILD_TYPE" == "mipv6-daemon" ]]; then
+        git remote add origin "git://git.umip.org/umip.git";
+    elif [[ "$BUILD_TYPE" == "libqmi" ]]; then
+        git remote add origin 'git://anongit.freedesktop.org/libqmi';
+    elif [[ "$BUILD_TYPE" == "libibverbs" ]]; then
+        git remote add origin 'git://git.kernel.org/pub/scm/libs/infiniband/libibverbs.git';
+    elif [[ "$BUILD_TYPE" == "initscripts" ]]; then
+        git remote add origin "https://git.fedorahosted.org/git/initscripts.git";
+    elif [[ "$BUILD_TYPE" == "iproute" ]]; then
+        git remote add origin "git://git.kernel.org/pub/scm/linux/kernel/git/shemminger/iproute2.git"
+    elif [[ "$BUILD_TYPE" == "dracut" ]]; then
+        git remote add origin "https://github.com/dracutdevs/dracut.git"
+    elif [[ "$BUILD_TYPE" == "systemd" ]]; then
+        git remote add origin "https://github.com/systemd/systemd.git"
+    elif [[ "$BUILD_TYPE" == "vpnc" ]]; then
+        git_remote_add_github ndpgroup/vpnc
+    elif [[ "$BUILD_TYPE" == "cloud-init" ]]; then
+        git remote add origin "https://git.launchpad.net/cloud-init"
+    elif [[ "$BUILD_TYPE" == "firewalld" ]]; then
+        git remote add origin "https://github.com/firewalld/firewalld.git"
+    elif [[ "$BUILD_TYPE" == "nftables" ]]; then
+        git remote add origin "git://git.netfilter.org/nftables"
+    elif [[ "$BUILD_TYPE" == "ulogd" ]]; then
+        git remote add origin "https://git.netfilter.org/ulogd2"
+    elif [[ "$BUILD_TYPE" == "libnetfilter_log" ]]; then
+        git remote add origin "https://git.netfilter.org/$BUILD_TYPE"
+    fi
+    LOCAL_MIRROR_URL="$(LANG=C git remote -v | sed -n 's/^origin\t*\([^\t].*\) (fetch)/\1/p')"
+    LOCAL_MIRROR="$(get_local_mirror "$LOCAL_MIRROR_URL")"
+    if [[ -n "$LOCAL_MIRROR" ]]; then
+        git remote add local-mirror "$LOCAL_MIRROR"
+        git fetch local-mirror
+    fi
+    LOCAL_GIT="$(readlink -f ../.git/local/)"
+    if [[ -d "$LOCAL_GIT" ]]; then
+        git remote add local "$LOCAL_GIT/"
+        git fetch local
+    fi
+    if [[ "$(git remote | grep '^origin$')x" != x && -z "$NO_REMOTE" ]]; then
+        git fetch origin
+        if [[ -n "$LOCAL_MIRROR" ]]; then
+            git remote rm local-mirror
+        fi
+    fi
+    git commit --allow-empty -m '*** empty initial commit'  # useful, to rebase the following commit
+    git add -f -A .
+    git commit -m '*** add all'
+    git tag -f ALL
+    ORIG_HEAD="`git rev-parse HEAD`"
+    if [[ "x$RELEASE_BASE_COMMIT" == x ]]; then
+        # if RELEASE_BASE_COMMIT is not set, try detecting the BASE_COMMIT...
+
+        if [[ "$BUILD_TYPE" == "NetworkManager" ||
+              "$BUILD_TYPE" == "NetworkManager-fortisslvpn" ||
+              "$BUILD_TYPE" == "NetworkManager-libreswan" ||
+              "$BUILD_TYPE" == "NetworkManager-pptp" ||
+              "$BUILD_TYPE" == "NetworkManager-openconnect" ||
+              "$BUILD_TYPE" == "NetworkManager-vpnc" ]]; then
+            RELEASE_BASE_COMMIT="$(sed -n 's/^NM_GIT_SHA=\(.*\)/\1/p' configure 2>/dev/null)"
+        elif [[ "$BUILD_TYPE" == "libnl3" ]]; then
+            RELEASE_BASE_COMMIT="$(sed -n 's/^LIBNL_GIT_SHA=\(.*\)/\1/p' configure 2>/dev/null)"
+            if [[ "$RELEASE_BASE_COMMIT" == "23c44dad998f72f39fd1fc24aa9579fd0a7f05c0" ]]; then
+                RELEASE_BASE_COMMIT="e01b9df629e2f4f833fdc4fe0bda460bb738d136"
+            fi
+        elif [[ "$BUILD_TYPE" == "network-manager-applet" ||
+                "$BUILD_TYPE" == "libnma" ]]; then
+            RELEASE_BASE_COMMIT="$(sed -n 's/^NMA_GIT_SHA=\(.*\)/\1/p' configure 2>/dev/null)"
+            if [[ "$RELEASE_BASE_COMMIT" == "8d8e34f22d5fae476eda96cf36d828c3ae8b63d3" ]]; then
+                RELEASE_BASE_COMMIT="a2377d7534780b96a32405cce2e5548e81bbd081"
+            fi
+        elif [[ "$BUILD_TYPE" == "glib2" ]]; then
+            RELEASE_BASE_COMMIT="$(git rev-parse --verify -q "$(sed 's/.*\<glib-\([0-9]\+\.[0-9]\+\.[0-9]\+\)\.[a-z0-9_.]\+\>.*$/\1/' ../sources)^{commit}" 2>/dev/null)"
+        elif [[ "$BUILD_TYPE" == "iproute" ]]; then
+            RELEASE_BASE_COMMIT="$(git rev-parse --verify -q "$(sed 's/.*\<iproute2-\([0-9]\+\.[0-9]\+\.[0-9]\+\)\..*/v\1/' ../sources)^{commit}" 2>/dev/null)"
+        elif [[ "$BUILD_TYPE" == "NetworkManager-openvpn" ]]; then
+            RELEASE_BASE_COMMIT="$(sed -n 's/^NM_GIT_SHA=\(.*\)/\1/p' configure 2>/dev/null)"
+            if [[ "x$RELEASE_BASE_COMMIT" == x ]]; then
+                DATE="$(sed -n 's/%global snapshot .git\(20[0-3][0-9]\)\([0-1][0-9]\)\([0-3][0-9]\)/\1-\2-\3/p' "../$SPEC")"
+                if [[ "x$DATE" != x ]]; then
+                    RELEASE_BASE_COMMIT="$(git rev-list -n1 --date-order --before="$DATE" origin/master 2>/dev/null)"
+                fi
+            fi
+        fi
+        if [[ "x$RELEASE_BASE_COMMIT" == x ]]; then
+            KNOWN_BASE_COMMITS="$(cat <<EOF
+# NetworkManager
+08670c9163a5d0f15c57c7891ef899eb125d9423  7251704430cb206f2c29bfebc45bd0fb *NetworkManager-0.9.9.0.git20131003.tar.bz2
+
+# ModemManager
+397761c9758c3a8c2d130afaf36dab645d6e0ecf  d9d93d2961ee35b4cd8a75a6a8631cb4  ModemManager-1.6.0.tar.xz
+b23413a064f03fb2f2214fb32164bcb4b7037c45  67160b94c0eda90ebf95d1b620229ca1  ModemManager-1.6.10.tar.xz
+526ec556bdf440fce3c48d3127836cf9d0b4501b  SHA512 (ModemManager-1.10.8.tar.xz) = c021939322be39e102371219e648d6acb3bc5b48cf570e02113ce559321155bfe5476b4012fd95f878c0a5c5d3b9d88fb19e95adec16a5b62a01581915a39f71
+
+# libnl3
+1a510c57e905c4beb06122b9688162c82d9b044f  d1111959652bd6ad87b2071f61c8c20c *libnl-doc-3.2.24.tar.gz
+83c762d7cf6a6c54831e8d684b22804f497704c4  6fe7136558a9071e70673dcda38545b3 *libnl-3.2.21.tar.gz
+c4d846f239036c05f516c1c71789e980b64b1e70  2e1c889494d274aca24ce5f6a748e66e *libnl-3.2.22.tar.gz
+0446731124bea8c1b447cc52a5ad5ae5750810ff  636769646f5b81b0caead81eab151b45 *libnl-3.2.25-rc1.tar.gz
+bd0e87b3d81d2498c3f35d5497771828bf04e017  e34999eaa184c84b315a8dff8afa4219  libnl-3.2.28-rc1.tar.gz
+656f381ccf58785319bb0236595c896125d33ed0  bab12db1eb94a42129f712a44be91a67  libnl-3.2.28.tar.gz
+
+# NetworkManager-applet
+5d4f17e205f71972d4143f9760426a366b4129d7  9cc0e383c216d4bc31622a0cfb53aaa7 *network-manager-applet-0.9.9.0.git20140123.5d4f17e.tar.bz2
+36c868498f09eacafcdce9d6b68ca5aeffaae899  3146f3ac3c30996a96cd2c602fbc81e1 *network-manager-applet-0.9.10.3.git20150511.36c8684.tar.bz2
+2d5b36cf69ea6d5e11726d479012c8ad7d6fd9fc  7fc2ed3f0c46ed41ddabe99d51513b1c *network-manager-applet-1.0.4.tar.xz
+
+# NetworkManager-libreswan, NetworkManager-openswan
+64c90fd50e57854a3fff3784b92814ffa8159b05  6a373868f85ac3b7c953f7fd6c76e637 *NetworkManager-openswan-0.9.8.0.tar.xz
+78555150e4df29eb39fa4a105f884f53b0f4523f  df9144805f37dc30dfaeab8da762f615 *NetworkManager-openswan-1.0.6.tar.xz
+3ef831cf25e86675f9838bf58b1cd6e592c6e14f  01248eb95a1e1d647057a45aed85a3af *NetworkManager-libreswan-1.2.4.tar.xz
+
+# NetworkManager-vpnc
+89bdcd324f2e257eca59168a7d0be5608438aab0  abb26a6c3c8d6c1d91c78471aff86b3a *NetworkManager-vpnc-0.9.8.2.tar.xz
+c37a79d43ebe1192ba8dcc5036cd668631b6473e  d87db7021629cef7c110a371dd42b7a8 *NetworkManager-vpnc-0.9.9.0.git20140131.tar.bz2
+68ca41550f9289835ea9d80e1ee059322ebe749a  4c16379738264a117d09c171c645ff23 *NetworkManager-vpnc-1.2.2.tar.xz
+
+# NetworkManager-openvpn
+1f159f30617e4a3b8121074b8bf238312941370d  511eae0d4ac17c6d2659a3da2646296f *NetworkManager-openvpn-1.0.2.tar.xz
+75585a94b394c04e45a28d2b032fe83dcdaeebee  ee4c09a8896eab3e1740f7c7bc1434f9  NetworkManager-openvpn-1.2.4.tar.xz
+
+# mipv6-daemon
+428974c2d0d8e75a2750a3ab0488708c5dfdd8e3  8e3ebd242e7926822bbdf5ce77c1d076 *mipv6-daemon-1.0.tar.gz
+
+# libqmi
+7d688f382f9756027bf92338e413e425365d2835  17d6c2b404ee1eb4d1e60050fef64491 *libqmi-1.6.0.tar.xz
+49abf405f5e9f16542476dceeb20de6029edcf1c  SHA512 (libqmi-1.24.0.tar.xz) = e899765e67c1db0f758030e78b296015c476f938bb2afa01594b3e71a0b8d5fc2237c8272497aec891d5555523ecf0fecd69c4d0e14165c07072780621b3b502
+
+# gnome-control-center
+e87e0361b117f055ace2aa47cdddd0dc62a852f9  da949e268254af6aafdda0e8c1702384 *gnome-control-center-3.22.1.tar.xz
+
+# wpa_supplicant
+22760dd94722a61175ff90c59d88c4cda1ed5e23  3be2ebfdcced52e00eda0afe2889839d *wpa_supplicant-2.0.tar.gz
+
+# libibverbs
+990ca025d0ad967b6f266bae700bf82a4ceaff1a  1fe85889c8bbc4968b1feba6524ca408 *libibverbs-1.1.8.tar.gz
+
+# initscripts
+cc304f05edab6c408a0f061eb1a104f9f06b8587  86ef789876b65c61751ce854835b91d4  initscripts-9.49.35.tar.bz2
+
+# dracut
+00efe708cab023bfe6eaf530d8ac8ea97b440de2  SHA512 (dracut-050.tar.xz) = 9d9a66acfd6b9d2fd50855a59a2393e0602c2ef97119db046f68d6167ea84d1423fa465b8b4d96339febb38d5a96df26dec7862c4b3397c3d726db18d280eee4
+
+# systemd
+903dd65b5eb63257393955cb79777beb8c71afc1  SHA512 (systemd-253-rc1.tar.gz) = aaf0a6bf21bbc50a42015c9cb17f69d1aaf6cab6cabfba5140a94212fb864e38d638dace9a70447f62b4d2a817a0d3bd6f4ae8d9b3c2e741cdeb1cb332f70b65
+
+# libnetfilter_log
+97866a0a7482ca518bad39536c7c667bfb9604b2  2a4bb0654ae675a52d2e8d1c06090b94  libnetfilter_log-1.0.1.tar.bz2
+b0e4be94c0b8f68d4e912402b93a130063c34e17  SHA512 (libnetfilter_log-1.0.2.tar.bz2) = 6b33718b1dd7f4504bceae14001da3a652cec46a6725a5dee83a7b55028cfa8e768cba917f968a5d5b60fd9ff04edf6040ef271a68e5fb65858bf73f4f9ccf23
+
+# ulogd
+79aa980f2df9dda0c097e8f883a62f414b9e5138  SHA512 (ulogd-2.0.8.tar.bz2) = 9f99f6f35bad5da4559d788dc3ba3dae17d4ae972737cae3313ecf68f08eaf5f55514fce6f30503437e4158fd30a06438b9249d5d20f6343964cbf690f87309d
+EOF
+)"
+            OLDIFS="$IFS"
+            IFS=$'\n'
+            for KNOWN_BASE_COMMIT in $KNOWN_BASE_COMMITS; do
+                MATCH="$(echo "$KNOWN_BASE_COMMIT" | sed -n 's/^[0-9a-f]\{40\} \+\(.*\)$/\1/p')"
+                if [[ "x$MATCH" == x ]]; then
+                    continue
+                fi
+                if grep -q "$MATCH" ../sources; then
+                    RELEASE_BASE_COMMIT="$(echo "$KNOWN_BASE_COMMIT" | awk '{print $1}')"
+                    break
+                fi
+            done
+            IFS="$OLDIFS"
+        fi
+    fi
+    if [[ x != "x$RELEASE_BASE_COMMIT" ]]; then
+        if [[ "$RELEASE_BASE_COMMIT" == "-" ]]; then
+            # you can disable detection of the RELEASE_BASE_COMMIT by setting it to '-'
+            RELEASE_BASE_COMMIT=
+        else
+            # verify the base commit...
+            RELEASE_BASE_COMMIT2="$(git rev-parse --verify -q "$RELEASE_BASE_COMMIT^{commit}" 2>/dev/null)"
+            [[ x == "x$RELEASE_BASE_COMMIT2" ]] && test -z "$NO_REMOTE" && die "error detecting RELEASE_BASE_COMMIT=$RELEASE_BASE_COMMIT"
+            RELEASE_BASE_COMMIT="$RELEASE_BASE_COMMIT2"
+        fi
+    fi
+    if [[ x != "x$RELEASE_BASE_COMMIT" ]]; then
+        git checkout -B master "$RELEASE_BASE_COMMIT" || die "could not checkout master"
+        git tag -f BASE
+        git rm --cached -r :/
+        git checkout "$ORIG_HEAD" -- :/
+        git clean -fdx :/
+        git commit -m '*** add all'
+        git tag -f ALL
+        [[ x == "x$(git diff HEAD "$ORIG_HEAD")" ]] || die "error recreating initial tarball"
+    fi
+    (
+        if [[ -n "$MAKEREPO_GIT_IGNORE_MY" ]]; then
+            cat "../.git/$MAKEREPO_GIT_IGNORE_MY"
+        fi
+        if [[ -f "../.git/$MAKEREPO_GIT_IGNORE_LAST" ]]; then
+            cat "../.git/$MAKEREPO_GIT_IGNORE_LAST"
+        fi
+        sed -n 's/^%patch\([0-9]\+\) \+.*-b \+\([^ ]\+\).*$/*\2/p' ../"$SPEC";
+        echo '*.[0-9][0-9][0-9][0-9][-.]*.orig'
+    ) | LANG=C sort | LANG=C uniq > .gitignore
+
+    git rm --cached -r .
+    git add --all .
+    git commit -m "*** clean state (ignored files removed)"
+    git tag -f CLEAN
+
+    if [[ "$REVERT_COUNT" == "" || $REVERT_COUNT -gt 0 ]]; then
+
+        # parse the list of patches
+        IFS=$'\n' read -rd '' -a PATCH_LIST <<<"$(sed -n 's/^Patch\([0-9]\+\):[ 	]\+\(.*\)$/\1 \2/p' ../"$SPEC" | sort -n)"
+
+        if [[ "$BUILD_TYPE" == "NetworkManager" ]]; then
+            if containsElement idx "123 rh1085015-applet-translations.patch" "${PATCH_LIST[@]}"; then
+                # for rhel-6, NetworkManager contains some patches that break the script. In this
+                # case, truncate the list of what we would normally revert.
+                PATCH_LIST=("${PATCH_LIST[@]:$((idx+1))}")
+            fi
+        fi
+
+        # truncate the list of patches to revert/reapply
+        if [[ "$REVERT_COUNT" == "" || "$REVERT_COUNT" -gt ${#PATCH_LIST[@]} ]]; then
+            echo "revert all ${#PATCH_LIST[@]} patches"
+        else
+            echo "revert the last $REVERT_COUNT patches of ${#PATCH_LIST[@]}"
+            PATCH_LIST=("${PATCH_LIST[@]:$((${#PATCH_LIST[@]} - $REVERT_COUNT))}")
+        fi
+
+        # split the list in index and patch file name
+        PATCH_LIST_N=()
+        for i in ${!PATCH_LIST[@]}; do
+            LAST_PATCH_N[$i]=$(echo "${PATCH_LIST[$i]}" | sed -n 's/^\([0-9]\+\) \+.*$/\1/p')
+            LAST_PATCH[$i]=$(  echo "${PATCH_LIST[$i]}" | sed -n 's/^\([0-9]\+\) \+\(.*\)$/\2/p')
+        done
+
+        # revert and patches in reverse order...
+        BASECOMMIT=("`git rev-parse HEAD`")
+        for j in "${!PATCH_LIST[@]}"; do
+            i=$((${#PATCH_LIST[@]} - $j - 1))
+            echo "revert Patch${LAST_PATCH_N[$i]} \"${LAST_PATCH[$i]}\"..."
+            PNUM="$(spec_parse_patch_p "../$SPEC" "${LAST_PATCH_N[$i]}")"
+            patch -f --no-backup-if-mismatch -R "-p$PNUM" < "../${LAST_PATCH[$i]}" || (
+                # error applying patch. Maybe we have a multi line patch...
+
+                rm -f "../${LAST_PATCH[$i]}".makerepo-split.*
+                split_patch "../${LAST_PATCH[$i]}" ".makerepo-split."
+
+                git reset --hard
+                git clean -fdx
+                for p in "../${LAST_PATCH[$i]}".makerepo-split.*; do
+                    echo ">>> try split part $p for ${LAST_PATCH[$i]}"
+                    patch --no-backup-if-mismatch -R "-p$PNUM" < "$p" || die "error reverting Patch${LAST_PATCH_N[$i]} ${LAST_PATCH[$i]}"
+                done
+            )
+            git add --all .
+            git commit --allow-empty -a -m "<< revert Patch${LAST_PATCH_N[$i]} \"${LAST_PATCH[$i]}\"$(get_patch_origin "${LAST_PATCH[$i]}")"
+            BASECOMMIT=("`git rev-parse HEAD`" "${BASECOMMIT[@]}")
+            git tag -f REVERT"${LAST_PATCH_N[$i]}"
+        done
+
+        # reapply the patches
+        for i in ${!PATCH_LIST[@]}; do
+            echo "reapply Patch${LAST_PATCH_N[$i]} \"${LAST_PATCH[$i]}\"..."
+
+            # create an empty commit, indicating the commit before starting to reapply
+            BASECOMMIT_REVERT="${BASECOMMIT[$((i))]}"
+            COMMIT_MSG="$(git log -n1 --format='%s%n%n%b' "$BASECOMMIT_REVERT" | sed '1s/<< revert \(Patch.*"\)$/-- before reapplying \1/')"
+            git commit --allow-empty -m "$COMMIT_MSG"
+            git tag -f "BEFORE_PATCH${LAST_PATCH_N[$i]}"
+            git tag -f "LAST0"
+
+            # first try git-am to preserve the commit message, otherwise just revert the last commit
+            if git am "../${LAST_PATCH[$i]}"; then
+                # The tree to the version before should be identical after reapplying the patch.
+                # Just to be sure, reset the commit.
+                git reset "${BASECOMMIT[$((i+1))]}" -- .
+                COMMIT_MSG="$(git log -n1 --format='%s%n%n%b' "$BASECOMMIT_REVERT" | sed '1s/<< revert \(Patch.*"\)$/-- after reapplying \1\n\ngit-am did not fully restore the previous state/')"
+                git commit -m "$COMMIT_MSG" || echo "NOTHING TO COMMIT"
+            else
+                git am --abort
+                git reset "${BASECOMMIT[$((i+1))]}" -- .
+                COMMIT_MSG="$(git log -n1 --format='%s%n%n%b' "$BASECOMMIT_REVERT" | sed '1s/<< revert \(Patch.*"\)$/>> reapply \1/')"
+                git commit --allow-empty -m "$COMMIT_MSG"
+            fi
+            git reset --hard HEAD
+            git clean -fdx
+            [[ x = "x$(git diff "${BASECOMMIT[$((i+1))]}" HEAD)" ]] || die "error reverting patch"
+            git tag -f PATCH"${LAST_PATCH_N[$i]}"
+        done
+        git tag -f LAST
+    fi
+    git checkout "$ORIG_HEAD" -- .
+    git checkout HEAD~ -- .gitignore
+    git reset
+
+    git gc
+popd
+
+if [[ $LOCAL != 0 ]]; then
+    rm -rf ./.makerepo.git/
+    mv "$DIRNAME/.git" ./.makerepo.git/
+    $FEDPKG $DIST local -- --noclean
+    mv ./.makerepo.git/ "$DIRNAME/.git"
+    pushd "$DIRNAME"
+        git checkout -- .gitignore
+
+        # write git-ignore file...
+        git status --porcelain | sed 's/^...//' >> "../.git/$MAKEREPO_GIT_IGNORE_LAST"
+    popd
+fi
+
+echo SUCCESS;
diff --git a/contrib/scripts/NM-log b/contrib/scripts/NM-log
new file mode 100755
index 00000000..85faea86
--- /dev/null
+++ b/contrib/scripts/NM-log
@@ -0,0 +1,85 @@
+#!/bin/bash
+
+# Util to pretty-print logfile of NetworkManager
+#
+# Unless setting NM_LOG_NO_COLOR it will colorize the output.
+# Suppress coloring with:
+# $ NM_LOG_NO_COLOR=1 NM-log ...
+#
+# If called without arguments, it either reads from stdin (if not
+# connected to a terminal) or it shows the journal content.
+#
+# If called with first argument "j", it always shows the journal content.
+#
+# You can pass multiple filenames.
+
+if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
+    NM_not_sourced=1
+else
+    unset NM_not_sourced
+fi
+
+NM-show-journal() {
+    local since="$(systemctl show NetworkManager | sed -n 's/^ExecMainStartTimestamp=\(.*\) [A-Z0-9]\+$/\1/p')"
+
+    if [[ "$since" == "" ]]; then
+       echo "error detecting NM. Is it running?"
+       systemctl status NetworkManager
+    else
+       journalctl -o short-precise --since "$since" -b 0 -u NetworkManager "$@"
+    fi
+}
+
+NM-colorize() {
+    if [[ "$NM_LOG_NO_COLOR" == "" ]]; then
+        # poor man's coloring using grep.
+        # TODO: do it somehow better (and more efficient).
+        sed 's/\r$//' | \
+        GREP_COLOR='01;31' grep -a --color=always '^\|^\(.* \)\?<\(warn> \|error>\) \[[0-9.]*\]' | \
+        GREP_COLOR='01;33' grep -a --color=always '^\|^\(.* \)\?<info>  \[[0-9.]*\]\( .*\<is starting\>.*$\)\?' | \
+        GREP_COLOR='01;37' grep -a --color=always '^\|\<platform:\( (.*)\)\? signal: .*$' | \
+        GREP_COLOR='01;34' grep -a --color=always '^\|\<platform\(-linux\)\?:\( (.*)\)\? link: \(add\|adding\|change\|setting\|deleting\|enslaving to master\|releasing \([0-9]\+ \)\?from master\)\>\|\<platform: routing-rule: \(adding or updating:\|delete \)\|\<platform:\( (.*)\)\? address: \(deleting\|adding or updating\) IPv. address:\? \|\<platform:\( (.*)\)\? \(route\|ip4-route\|ip6-route\|qdisc\|tfilter\): \([a-z]\+\|adding or updating\|new\[0x[0-9A-Za-z]*\]\) \|\<platform-linux: sysctl: setting ' | \
+        GREP_COLOR='01;35' grep -a --color=always '^\|\<audit: .*$' | \
+        GREP_COLOR='01;32' grep -a --color=always '^\|\<device (.*): state change: ' |
+        if [[ "$NM_LOG_GREP" != "" ]]; then
+            GREP_COLOR='01;36' grep -a --color=always "^\\|$NM_LOG_GREP"
+        else
+            /bin/cat -
+        fi
+    else
+        /bin/cat -
+    fi
+}
+
+NM-log() {
+    local NM_LOG_GREP=
+
+    while [[ $# -gt 0 ]]; do
+        if [[ "$1" == "-h" ]]; then
+            shift
+            NM_LOG_GREP="${NM_LOG_GREP+$NM_LOG_GREP\\|}\\<$1\\>"
+            shift
+        else
+            break
+        fi
+    done
+
+    (
+        if [ "$1" == "j" ]; then
+            shift
+            NM-show-journal "$@"
+        elif [ "$#" -eq 0 -a -t 0 ]; then
+            NM-show-journal
+        else
+            a="${1--}"
+            shift
+            /usr/bin/less -f "$a" "$@"
+        fi
+    ) | \
+        NM_LOG_GREP="$NM_LOG_GREP" NM-colorize | \
+        LESS=FRSXM less -f -R --shift=5
+}
+
+if [[ "$NM_not_sourced" != "" ]]; then
+    NM-log "$@"
+fi
diff --git a/contrib/scripts/anonymize-logs.py b/contrib/scripts/anonymize-logs.py
new file mode 100755
index 00000000..36b82ed1
--- /dev/null
+++ b/contrib/scripts/anonymize-logs.py
@@ -0,0 +1,193 @@
+#!/usr/bin/env python3
+
+from textwrap import wrap
+import subprocess
+import ipaddress
+import argparse
+import os
+import re
+
+
+domains = []
+
+hosts_sub = {}
+host_next = 0
+
+macs_sub = {}
+mac_next = 0
+
+ips_sub = {}
+ip4_next = ipaddress.IPv4Address("0.0.0.0")
+ip6_next = ipaddress.IPv6Address("ffff::")
+
+
+def main(args):
+    must_autoreplace_hostnames = not args.show_hostnames
+    must_replace_hostnames = must_autoreplace_hostnames or args.domain or args.hostname
+
+    init_hostnames_and_domains_sub(args)
+
+    with open(args.log_file) as f:
+        for line in (line.strip() for line in f):
+            if must_replace_hostnames:
+                line = replace_hostnames(line, must_autoreplace_hostnames)
+            if not args.show_macs:
+                line = replace_macs(line)
+            if not args.show_public_ips or args.hide_private_ips:
+                line = replace_ips(line, args.show_public_ips, args.hide_private_ips)
+
+            print(line)
+
+
+def init_hostnames_and_domains_sub(args):
+    global domains
+
+    if not args.show_hostnames:
+        domains.extend(["com", "org", "net", "gov", "es", "it"])
+
+        r = subprocess.run("hostname", capture_output=True)
+        if r.returncode == 0:
+            own_hostname = r.stdout.decode().strip()
+            add_host_sub(own_hostname, ".self")
+
+    # domains and hostname passed explicitly are replaced even with --show-hostnames
+    domains.extend(d.strip(". ") for d in args.domain)
+    domains = "|".join(domains)
+
+    for hostname in args.hostname:
+        add_host_sub(hostname)
+
+
+def add_host_sub(hostname: str, suffix: str = ""):
+    global hosts_sub
+    global host_next
+
+    # if it's a domain-like hostname (i.e example.com) adds .ext at the end
+    if suffix == "" and re.search(r"\.({})$".format(domains), hostname):
+        suffix = ".ext"
+
+    if hostname not in hosts_sub:
+        hosts_sub[hostname] = "hostname{}{}".format(host_next, suffix)
+        host_next += 1
+
+
+def replace_hostnames(line: str, autodetect_from_logs: bool) -> str:
+    global hosts_sub
+
+    # look for known log messages that show hostnames
+    if autodetect_from_logs:
+        match = re.search(r"get-hostname: \"(.*)\"", line)
+        if match:
+            add_host_sub(match.group(1))
+
+        match = re.search(r"set hostname to \"(.*)\"", line)
+        if match:
+            add_host_sub(match.group(1))
+
+        match = re.search(
+            r"hostname changed from (\(none\)|\".*\") to (\(none\)|\".*\")", line
+        )
+        if match:
+            if match.group(1) != "(none)":
+                add_host_sub(match.group(1).strip('"'))
+            if match.group(2) != "(none)":
+                add_host_sub(match.group(2).strip('"'))
+
+    # look for domain-like strings
+    if domains:
+        match = re.search(r"[\w\-\.]+?\.(" + domains + r")\b", line)
+        if match:
+            add_host_sub(match.group(0))
+
+    for orig, repl in hosts_sub.items():
+        line = line.replace(orig, repl)
+
+    return line
+
+
+def replace_macs(line: str) -> str:
+    global macs_sub
+    global mac_next
+
+    macs = re.findall(r"(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}", line)
+
+    for mac in macs:
+        if mac not in macs_sub:
+            macs_sub[mac] = ":".join(wrap("{:012x}".format(mac_next), width=2))
+            mac_next += 1
+
+        line = line.replace(mac, macs_sub[mac])
+
+    return line
+
+
+def replace_ips(line: str, show_public: bool, hide_private: bool) -> str:
+    global ips_sub
+    global ip4_next
+    global ip6_next
+
+    ips4 = re.findall(r"(?:[0-9]{1,3}\.){3}[0-9]{1,3}", line)
+    ips6 = re.findall(r"(?:[0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}", line)
+
+    for addr_str in ips4 + ips6:
+        try:
+            addr = ipaddress.ip_address(addr_str)
+        except:  # not IP
+            continue
+
+        if (addr.is_private and not hide_private) or (addr.is_global and show_public):
+            continue
+
+        if addr.exploded not in ips_sub:
+            if type(addr) is ipaddress.IPv4Address:
+                ips_sub[addr.exploded] = str(ip4_next).replace("0.", "IP4.", 1)
+                ip4_next += 1
+            else:
+                ips_sub[addr.exploded] = str(ip6_next).replace("ffff:", "IPv6:", 1)
+                ip6_next += 1
+
+        line = line.replace(addr_str, ips_sub[addr.exploded])
+
+    return line
+
+
+if __name__ == "__main__":
+    args_parser = argparse.ArgumentParser(
+        prog=os.path.basename(__file__),
+        description="""Anonymize some data from NetworkManager logs.
+
+Note that it only covers some common stuff like MAC and IP addresses or
+hostnames.  Do not trust it and manually review that the log doesn't contain
+sensitive data before sharing it.
+
+Changing IP address can make that problems related to routing are impossible to
+analyze. Because of that, private IPs which are normally not sensitive are not
+hidden by default, and if the problem is related to routing you might need to
+use the --show-public-ips option""",
+        epilog="Options of the type --show-* disable masking that type of data.",
+        formatter_class=argparse.RawTextHelpFormatter,
+    )
+    args_parser.add_argument("-H", "--show-hostnames", action="store_true")
+    args_parser.add_argument("-m", "--show-macs", action="store_true")
+    args_parser.add_argument("-g", "--show-public-ips", action="store_true")
+    args_parser.add_argument("-p", "--hide-private-ips", action="store_true")
+    args_parser.add_argument(
+        "-d",
+        "--domain",
+        action="append",
+        default=[],
+        help='additional domains to hide, like ".xyz", can be passed more than once',
+    )
+    args_parser.add_argument(
+        "-n",
+        "--hostname",
+        action="append",
+        default=[],
+        help="additional hostnames to hide, can be passed more than once",
+    )
+    args_parser.add_argument(
+        "log_file", nargs="?", default="/dev/stdin", help="Log file (by default, stdin)"
+    )
+
+    args = args_parser.parse_args()
+    main(args)
diff --git a/contrib/scripts/btmodem.pl b/contrib/scripts/btmodem.pl
new file mode 100755
index 00000000..feaa32e0
--- /dev/null
+++ b/contrib/scripts/btmodem.pl
@@ -0,0 +1,291 @@
+#!/usr/bin/env perl
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+# Copyright (C) 2019 Red Hat, Inc.
+
+# $ perldoc btmodem.pl if you'd like to read the manual, poor you:
+
+=head1 NAME
+
+btmodem.pl - emulate a bluetooth DUN modem
+
+=head1 SYNOPSIS
+
+btmodem.pl [<hci>] [-- <pppd> ...]
+
+=head1 DESCRIPTION
+
+B<btmodem.pl> registers a Bluetooth DUN profile with Bluez, accepts incoming
+connections and pretends there's modem there.
+
+It answers a basic subset of AT commands, sufficient making ModemManager
+recognize it as a 3GPP capable modem registered to a network.
+
+Upon receiving the dial (ATD) command, it spawns C<pppd> so that
+NetworkManager can establish a connection.
+
+=head1 OPTIONS
+
+=over 4
+
+=item B<< <hci> >>
+
+Create a service on this particular HCI.
+
+Defaults to I<hci0>.
+
+=item B<< <pppd> >>
+
+Specifies extra arguments to be prepended before C<pppd> to the default
+set of I<nodetach notty local logfd 2 nopersist>.
+
+Defaults to I<pppd noauth dump debug 172.31.82.1:172.31.82.2>.
+
+=back
+
+=cut
+
+use strict;
+use warnings;
+
+use IO::Handle;
+use Net::DBus;
+use Net::DBus::Reactor;
+
+# Parse command line arguments
+my $hci_name;
+my @pppd = qw/pppd noauth dump debug 172.31.82.1:172.31.82.2/;
+while (@ARGV) {
+	$_ = shift @ARGV;
+	if ($_ eq '--') {
+		@pppd = @ARGV;
+		last;
+	} else {
+		die "Extra argument: '$_'" if $hci_name;
+		$hci_name = $_;
+	}
+};
+$hci_name ||= 'hci0';
+
+sub modemu
+{
+	my $fh = shift;
+
+	while (<$fh>) {
+		chomp;
+
+		if (/^AT$/ or /^ATE0$/ or /^ATV1$/ or /^AT\+CMEE=1$/ or /^ATX4$/ or /^AT&C1$/ or /^ATZ$/) {
+			# Standard Hayes commands that are basically used to
+			# ensure the modem is in a known state. Accept them all.
+			print $fh "\r\n";
+			print $fh "OK\r\n";
+
+		} elsif (/^AT\+CPIN\?$/) {
+			# PIN unlocked. Required.
+			print $fh "\r\n";
+			print $fh "+CPIN:READY\r\n";
+			print $fh "\r\n";
+			print $fh "OK\r\n";
+
+		} elsif (/^AT\+COPS=0$/) {
+			# Select access technology (we just accept 0=automatic)
+			print $fh "\r\n";
+			print $fh "OK\r\n";
+
+		} elsif (/^AT\+CGREG\?$/) {
+			# 3GPP Registration status.
+			print $fh "\r\n";
+			print $fh "+CGREG: 0,1\r\n";
+			print $fh "\r\n";
+			print $fh "OK\r\n";
+
+		} elsif (/^AT\+CGDCONT=\?$/) {
+			# Get supported PDP contexts
+			print $fh "\r\n";
+			print $fh "+CGDCONT: (1-10),(\"IP\"),,,(0-1),(0-1)\r\n";
+			print $fh "+CGDCONT: (1-10),(\"IPV6\"),,,(0-1),(0-1)\r\n";
+			print $fh "OK\r\n";
+
+		} elsif (/^AT\+CGACT=0,1$/) {
+			# Activate a PDP context
+			print $fh "\r\n";
+			print $fh "OK\r\n";
+
+		} elsif (/^AT\+CGDCONT=1,"(.*)","(.*)"$/) {
+			# Set PDP context. We accept any.
+			print $fh "\r\n";
+			print $fh "OK\r\n";
+
+		} elsif (/^ATD/) {
+			print $fh "\r\n";
+			print $fh "CONNECT 28800000\r\n";
+
+			my $ppp = fork;
+			die "Can't fork: $!" unless defined $ppp;
+			if ($ppp == 0) {
+				close STDIN;
+				close STDOUT;
+				open STDIN, '<&', $fh or die "Can't dup pty to a pppd stdin: $!";
+				open STDOUT, '>&', $fh or die "Can't dup pty to a pppd stdout: $!";
+				close $fh;
+				exec @pppd, qw/nodetach notty local logfd 2 nopersist/;
+				die "Can't exec pppd: $!";
+			}
+			waitpid $ppp, 0;
+		} else {
+			print $fh "\r\n";
+			print $fh "ERROR\r\n";
+		}
+	}
+}
+
+my $bus = Net::DBus->system;
+
+$bus->get_connection->register_object_path("/", sub {
+	my $bus = shift;
+	my $call = shift;
+
+	# We only support the NewConnection call
+	next unless $call->get_type eq &Net::DBus::Binding::Message::MESSAGE_TYPE_METHOD_CALL;
+	if (   $call->get_interface ne 'org.bluez.Profile1'
+	    or $call->get_path ne '/'
+	    or $call->get_member ne 'NewConnection'
+	    or $call->get_signature ne 'oha{sv}') {
+
+	       $bus->send ($bus->make_error_message (
+			replyto => $call,
+			name => ' org.freedesktop.DBus.Error.Failed',
+			description => "Forgive me caller for I don't know what to do"));
+		next;
+	}
+
+	my ($path, $fd, $args) = $call->get_args_list;
+	open (my $fh, "+>&=", $fd) or die $!;
+
+	my $pid = fork;
+	die unless defined $pid;
+
+	if ($pid == 0) {
+		# This allows us to use buffered read for lines from ModemManager
+		# despite not ending with \n
+		IO::Handle->input_record_separator ("\r");
+		$fh->autoflush (1);
+		$fh->blocking (1);
+		modemu ($fh);
+		exit 0;
+		die;
+	}
+
+	$bus->send ($bus->make_method_return_message ($call))
+		unless $call->get_no_reply;
+});
+
+my $bluez = $bus->get_service ('org.bluez');
+my $profile_manager = $bluez->get_object ('/org/bluez', 'org.bluez.ProfileManager1');
+
+$profile_manager->RegisterProfile('/', '00001103-0000-1000-8000-00805f9b34fb', {});
+
+Net::DBus::Reactor->main->run;
+
+=head1 SETTING UP BLUETOOTH
+
+In order for this script useful, you need to have two Bluetooth interfaces
+paired together. It's somewhat easier if you've got two machines to test.
+
+The pairing can be done withing the C<bluetoothctl> shell. Launch it after
+you started C<btmodem.pl>, so that the right profile UUIDs are discovered
+by the client. These commands come in handy:
+
+=over
+
+=item [bluetooth]# B<default-agent>
+
+This makes C<bluetoothctl> ask for pairing PIN in the shell session. That is
+useful if you're ssh-ing into a machine instead of using a desktop shell with
+its own agent. Run this on both machines.
+
+=item [bluetooth]# B<discoverable on>
+
+Broadcast the server service. You don't need to run this on the client.
+
+=item [bluetooth]# B<scan on>
+
+Turn on discovery of the devices. You need to don't run this on the server.
+
+After you've turned the discovery on, wait for a minute or so for your
+server to get discovered.
+
+=item [bluetooth]# B<devices>
+
+List the known devices, both those who've been discovered and those that have
+been paired with.
+
+=item [bluetooth]# B<pair 00:AA:01:00:00:23>
+
+Initiate the pairing. Run it from the machine that has scanning enabled.
+Assumes your server is C<00:AA:01:00:00:23> -- check your real address with the
+C<devices> command.
+
+After a short while, you should see the pairing confirmation prompt on both machines.
+
+=item [bluetooth]# B<trust 00:AA:01:00:00:24>
+
+Allow incoming connections from C<00:AA:01:00:00:24>. Run this on the server.
+
+=item B<nmcli c add type bluetooth ifname '*' gsm.apn internet bluetooth.type dun bluetooth.bdaddr 00:AA:01:00:00:23>
+
+If everything went right, you can now connect.
+
+=back
+
+=head1 EXAMPLES
+
+=over
+
+=item B<btmodem.pl>
+
+Just emulate a DUN modem on I<hci0>, with the default PPP arguments.
+
+=item B<btmodem.pl hci666>
+
+Same as above, just on the I<hci666> interface.
+
+=item B<btmodem.pl -- unshare --net pppd 172.31.82.1:172.31.82.2>
+
+Avoid polluting the namespace with the modem end of PPP connection.
+
+=item B<btmodem.pl -- pppd 10.0.0.1:10.0.0.2>
+
+Override the C<pppd> parameters: no debug logging and different set of
+addresses.
+
+=item B<btmodem.pl mymodem -- pppd 10.0.0.1:10.0.0.2>
+
+Same as above, with a modem name different from default.
+
+=back
+
+=head1 BUGS
+
+Haha. You tell me.
+
+=head1 SEE ALSO
+
+L<ModemManager(8)>, L<pppd(8)>, C<modemu.pl>
+
+=head1 COPYRIGHT
+
+Copyright (C) 2019 Lubomir Rintel
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+=head1 AUTHOR
+
+Lubomir Rintel C<lkundrak@v3.sk>
+
+Like, it's me who wrote it, but if you're running it it's your problem.
+
+=cut
diff --git a/contrib/scripts/checkpatch-feature-branch.sh b/contrib/scripts/checkpatch-feature-branch.sh
new file mode 100755
index 00000000..d6f72b20
--- /dev/null
+++ b/contrib/scripts/checkpatch-feature-branch.sh
@@ -0,0 +1,61 @@
+#!/bin/bash
+
+die() {
+    printf "%s\n" "$@"
+    exit 1
+}
+
+HEAD="${1:-HEAD}"
+
+BASE_DIR="$(dirname "$0")"
+
+if printf '%s' "$HEAD" | grep -q '\.\.'; then
+    # Check the explicitly specified range from the argument.
+    REFS=( $(git log --reverse --format='%H' "$HEAD") ) || die "not a valid range (HEAD is $HEAD)"
+else
+    BASE_REF="refs/remotes/origin"
+    NM_UPSTREAM_REMOTE=
+
+    if [ "$NM_CHECKPATCH_FETCH_UPSTREAM" == 1 ]; then
+        NM_UPSTREAM_REMOTE="nm-upstream-$(date '+%Y%m%d-%H%M%S')-$RANDOM"
+        git remote add "$NM_UPSTREAM_REMOTE" https://gitlab.freedesktop.org/NetworkManager/NetworkManager.git
+        BASE_REF="refs/remotes/$NM_UPSTREAM_REMOTE"
+        git fetch origin "$(git rev-parse "$HEAD")" --no-tags --unshallow
+        git fetch "$NM_UPSTREAM_REMOTE" \
+            --no-tags \
+            "refs/heads/main:$BASE_REF/main" \
+            "refs/heads/nm-*:$BASE_REF/nm-*" \
+            || die "failure to fetch from https://gitlab.freedesktop.org/NetworkManager/NetworkManager.git"
+    fi
+
+    # the argument is only a single ref (or the default "HEAD").
+    # Find all commits that branch off one of the stable branches or main
+    # and lead to $HEAD. These are the commits of the feature branch.
+
+    RANGES=( $(git show-ref | sed 's#^\(.*\) '"$BASE_REF/"'\(main\|nm-1-[0-9]\+\)$#\1..'"$HEAD"'#p' -n) )
+
+    [ "${#RANGES[@]}" != 0 ] || die "cannot detect git-ranges (HEAD is $(git rev-parse "$HEAD"))"
+
+    REFS=( $(git log --reverse --format='%H' "${RANGES[@]}") )
+
+    if [ "${#REFS[@]}" == 0 ] ; then
+        # no refs detected. This means, $HEAD is already on main (or one of the
+        # stable nm-1-* branches. Just check the patch itself.
+        REFS=( "$HEAD" )
+    fi
+
+    if [ -n "$NM_UPSTREAM_REMOTE" ]; then
+        git remote remove "$NM_UPSTREAM_REMOTE"
+    fi
+fi
+
+SUCCESS=0
+for H in "${REFS[@]}"; do
+    export NM_CHECKPATCH_HEADER=$'\n'">>> VALIDATE \"$(git log --oneline -n1 "$H")\""
+    git format-patch -U65535 --stdout -1 "$H" | "$BASE_DIR/checkpatch.pl"
+    if [ $? != 0 ]; then
+        SUCCESS=1
+    fi
+done
+
+exit $SUCCESS
diff --git a/contrib/scripts/checkpatch-git-post-commit-hook b/contrib/scripts/checkpatch-git-post-commit-hook
new file mode 100755
index 00000000..96479107
--- /dev/null
+++ b/contrib/scripts/checkpatch-git-post-commit-hook
@@ -0,0 +1,23 @@
+#!/bin/sh
+
+# contrib/scripts/checkpatch-git-post-commit-hook:
+#   Call this script via ".git/hooks/post-commit"
+
+DISABLED=${NM_HOOK_DISABLED:0}
+
+if [ "$DISABLED" == 1 ]; then
+    echo "COMMIT HOOK DISABLED"
+    exit 0
+fi
+
+FILE=contrib/scripts/checkpatch-feature-branch.sh
+if [ -x "$FILE" ]; then
+    "$FILE"
+    exit 0
+fi
+
+FILE=contrib/scripts/checkpatch.pl
+if [ -x "$FILE" ]; then
+    git format-patch -U65535 --stdout -1 | "$FILE"
+    exit 0
+fi
diff --git a/contrib/scripts/checkpatch.pl b/contrib/scripts/checkpatch.pl
new file mode 100755
index 00000000..c24db35e
--- /dev/null
+++ b/contrib/scripts/checkpatch.pl
@@ -0,0 +1,319 @@
+#!/usr/bin/perl -n
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# Copyright (C) 2018,2021 Red Hat, Inc.
+#
+
+# $ perldoc checkpatch.pl for eye-pleasing view of the manual:
+
+=head1 NAME
+
+checkpatch.pl - check for common mistakes
+
+=head1 SYNOPSIS
+
+checkpatch.pl [<file> ...]
+
+=head1 DESCRIPTION
+
+B<checkpatch.pl> checks source files or patches for common mistakes.
+
+=head1 OPTIONS
+
+=over 4
+
+=item B<< <file> >>
+
+A C source file or an unified diff.
+
+=back
+
+=cut
+
+use strict;
+use warnings;
+
+chomp;
+
+our $is_patch;
+our $is_file;
+our $is_commit_message;
+
+our $seen_error;
+our $line;		# Current line
+our $check_line;	# Complain if errors are found on this line
+
+our @functions_seen;
+our $type;
+our $filename;
+our $line_no;
+our $indent;
+our $check_is_todo;
+our $expect_spdx;
+our $subdir;
+
+sub new_hunk
+{
+	$type = undef;
+	$indent = undef;
+}
+
+sub new_file
+{
+	$expect_spdx = 0;
+	$check_is_todo = 1;
+	$filename = $subdir // '';
+	$filename .= shift;
+	@functions_seen = ();
+}
+
+my $header = $ENV{'NM_CHECKPATCH_HEADER'};
+
+sub complain
+{
+	my $message = shift;
+	my $plain_message = shift;
+
+	return unless $check_line;
+
+	if (defined($header)) {
+		warn "$header\n";
+		undef $header;
+	}
+
+	if ($plain_message) {
+		warn "$message\n";
+	} else {
+		warn "$filename:$line_no: $message:\n";
+		warn "> $line\n\n";
+	}
+	$seen_error = 1;
+}
+
+sub check_commit
+{
+	my $commit = shift;
+	my $required = shift;
+	my $commit_id;
+	my $commit_message;
+
+	if ($commit =~ /^([0-9a-f]{5,})\b/) {
+		$commit_id = $1;
+	} else {
+		return unless $required;
+	}
+
+	if ($commit_id and not system 'git rev-parse --git-dir >/dev/null 2>/dev/null') {
+		$commit_message = `git log --abbrev=12 --pretty=format:"%h ('%s')" -1 "$commit_id" 2>/dev/null`;
+		complain "Commit '$commit_id' does not seem to exist" unless $commit_message;
+	}
+
+	$commit_message //= "<12 hex digits> ('<commit subject>')";
+	complain "Refer to the commit id properly: $commit_message" unless $commit =~ /^[0-9a-f]{12} \('/;
+}
+
+if ($is_patch) {
+	# This is a line of an unified diff
+	if (/^@@.*\+(\d+)/) {
+		$line_no = $1 - 1;
+		new_hunk;
+		next;
+	}
+	if (/^\+\+\+ (b\/)?(.*)/) {
+		new_file ($2);
+		next;
+	}
+	s/^([ \+])(.*)/$2/ or next;
+	$line_no++;
+	$check_line = $1 eq '+';
+	$line = $2;
+} elsif ($is_file) {
+	$line_no = $.;
+	$. = 0 if eof;
+	# This is a line from full C file
+	$check_line = 1;
+	$line = $_;
+} elsif ($is_commit_message) {
+	$line_no++;
+	$filename = '(commit message)';
+	$check_line = 1;
+	$line = $_;
+	/^---$/ and $is_commit_message = 0;
+	/^(Reverts|Fixes): *(.*)/ and check_commit ($2, 1);
+	/This reverts commit/ and next;
+	/cherry picked from/ and next;
+	/^git-subtree-dir: (.*)/ and $subdir = "$1/";
+	/\bcommit (.*)/ and check_commit ($1, 0);
+	next;
+} else {
+	# We don't handle these yet
+	/^diff --cc/ and exit 0;
+	$filename = '';
+	$line_no = 1;
+	# We don't know if we're dealing with a patch or a C file yet
+	$is_commit_message = 1 if /^From \S/;
+	$is_file = 1 if /^#/;
+	$is_patch = 1 if /^---/;
+	next;
+}
+
+if ($is_file and $filename ne $ARGV) {
+	new_file ($ARGV);
+	new_hunk;
+}
+
+if ($filename !~ /\.[ch]$/) {
+	if ($check_is_todo) {
+		complain("Resolve todo list \"$filename\" first\n", 1) if $filename =~ /^TODO.txt$/;
+		$check_is_todo = 0;
+	}
+	next;
+}
+
+next if $filename =~ /\/nm-[^\/]+-enum-types\.[ch]$/;
+next if $filename =~ /\b(shared|src)\/systemd\//
+	and not $filename =~ /\/sd-adapt\//
+	and not $filename =~ /\/nm-/;
+next if $filename =~ /\/(n-acd|c-list|c-siphash|n-dhcp4)\//;
+
+$expect_spdx = 1 if $line_no == 1;
+$expect_spdx = 0 if $line =~ /SPDX-License-Identifier/;
+complain ('Missing a SPDX-License-Identifier') if $line_no == 2 and $expect_spdx;
+
+complain ('Tabs are only allowed at the beginning of a line') if $line =~ /[^\t]\t/;
+complain ('Trailing whitespace') if $line =~ /[ \t]$/;
+complain ('Don\'t use glib typedefs for char/short/int/long/float/double') if $line =~ /\bg(char|short|int|long|float|double)\b/;
+complain ("Don't use \"$1 $2\" instead of \"$2 $1\"") if $line =~ /\b(char|short|int|long) +(unsigned|signed)\b/;
+complain ("Don't use \"unsigned int\" but just use \"unsigned\"") if $line =~ /\b(unsigned) +(int)\b/;
+complain ("Please use LGPL-2.1-or-later SPDX tag for new files") if $is_patch and $line =~ /SPDX-License-Identifier/ and not /LGPL-2.1-or-later/;
+complain ("Use a SPDX-License-Identifier instead of Licensing boilerplate") if $is_patch and $line =~ /under the terms of/;
+complain ("Don't use space inside elvis operator ?:") if $line =~ /\?[\t ]+:/;
+complain ("Don't add Emacs editor formatting hints to source files") if $line_no == 1 and $line =~ /-\*-.+-\*-/;
+complain ("XXX marker are reserved for development while work-in-progress. Use TODO or FIXME comment instead?") if $line =~ /\bXXX\b/;
+complain ("This gtk-doc annotation looks wrong") if $line =~ /\*.*\( *(transfer-(none|container|full)|allow none) *\) *(:|\()/;
+complain ("The gtk-doc annotation (allow-none) is deprecated. Use either (nullable) and/or (optional). See https://gi.readthedocs.io/en/latest/annotations/giannotations.html#deprecated-gobject-introspection-annotations") if $line =~ /\*.*\( *(allow-none) *\) *(:|\()/;
+complain ("Prefer nm_assert() or g_return*() to g_assert*()") if $line =~ /g_assert/ and (not $filename =~ /\/tests\//) and (not $filename =~ /\/nm-test-/);
+complain ("Use gs_free_error with GError variables") if $line =~ /\bgs_free\b +GError *\*/;
+complain ("Initialize GError variables to NULL, if you pass them on") if $line =~ /\bGError +\*([a-z0-9_]+);/;
+complain ("Don't use strcmp/g_strcmp0 unless you need to sort. Consider nm_streq()/nm_streq0(),NM_IN_STRSET() for testing equality") if $line =~ /\b(strcmp|g_strcmp0)\b/;
+complain ("Don't use API that uses the numeric source id. Instead, use GSource and API like nm_g_idle_add(), nm_g_idle_add_source(), nm_clear_g_source_inst(), etc.") if $line =~ /\b(g_idle_add|g_idle_add_full|g_timeout_add|g_timeout_add_seconds|g_source_remove|nm_clear_g_source)\b/;
+complain ("Prefer g_snprintf() over snprintf() (for consistency)") if $line =~ /\b(snprintf)\b/;
+complain ("Prefer nm_str_hash()/nm_direct_hash() over g_str_hash()/g_direct_hash(). Those use siphash24") if $line =~ /\b(g_str_hash|g_direct_hash)\b/;
+complain ("Don't use g_direct_equal() for hash tables, pass NULL for pointer equality which avoids the function call") if $line =~ /\b(g_direct_equal)\b/;
+complain ("Prefer nm_pint_hash()/nm_pint64_hash()/nm_pdouble_hash() over g_int_hash()/g_int64_hash()/g_double_hash(). Those use siphash24") if $line =~ /\b(g_int_hash|g_int64_hash|g_double_hash)\b/;
+complain ("Prefer nm_pint_equal()/nm_pint64_equal()/nm_pdouble_equal() over g_int_equal()/g_int64_equal()/g_double_equal(). Those names mirror our nm_p*_hash() functions") if $line =~ /\b(g_int_equal|g_int64_equal|g_double_equal)\b/;
+complain ("Avoid g_clear_pointer() and use nm_clear_pointer() (or nm_clear_g_free(), g_clear_object(), etc.)") if $line =~ /\b(g_clear_pointer)\b/;
+complain ("Define setting properties with _nm_setting_property_define_direct_*() API") if $line =~ /g_param_spec_/ and $filename =~ /\/libnm-core-impl\/nm-setting/;
+complain ("Use nm_g_array_{index,first,last,index_p}() instead of g_array_index(), as it nm_assert()s for valid element size and out-of-bound access") if $line =~ /\bg_array_index\b/;
+complain ("Use spaces instead of tabs") if $line =~ /\t/;
+complain ("Prefer implementing private pointers via _NM_GET_PRIVATE() or _NM_GET_PRIVATE_PTR() (the latter, if the private data has an opqaue pointer in the header file)") if $line =~ /\b(g_type_class_add_private|G_TYPE_INSTANCE_GET_PRIVATE)\b/;
+complain ("Don't use close()/g_close(). Instead, use nm_close() (or nm_close_with_error()).") if $line =~ /\b(close|g_close)\b *\(/;
+complain ("Use nm_memdup() instead of g_memdup(). The latter has a size argument of type guint") if $line =~ /\bg_memdup\b/;
+
+# Further on we process stuff without comments.
+$_ = $line;
+s/\s*\/\*.*\*\///;
+s/\s*\/\*.*//;
+s/\s*\/\/.*//;
+/^\s* \* / and next;
+
+if (/^typedef*/) {
+	# We expect the { on the same line as the typedef. Otherwise it
+	# looks too much like a function declaration
+	complain ('Unexpected line break following a typedef') unless /[;{,]$/;
+	next;
+} elsif (/^[A-Za-z_][A-Za-z0-9_ ]*\*?$/ and /[a-z]/) {
+	# A function type
+	$type = $_;
+	next;
+} elsif ($type and /^([A-Za-z_][A-Za-z0-9_]*)(\s*)\(/) {
+	my @order = qw/^get_property$ ^set_property$ (?<!_iface|_class)_init$ ^constructor$
+		^constructed$ _new$ ^dispose$ ^finalize$ _class_init$/;
+	my @following = ();
+	my @tmp = ();
+
+	# A function name
+	my $name = $1;
+	complain ('No space between function name and arguments') unless $2 eq '';
+
+	# Determine which function must not be preceding this one
+	foreach my $func (reverse @order) {
+		if ($name =~ /$func/) {
+			@following = @tmp;
+			last;
+		}
+		push @tmp, $func;
+	}
+
+	# Check if an out-of-order function was seen
+	foreach my $func (@following) {
+		my @wrong = grep { /$func/ } @functions_seen;
+		complain (join (', ', map { "'$_'" } @wrong)." should follow '$name'") if @wrong;
+	}
+
+	push @functions_seen, $1;
+	$type = undef;
+	next;
+}
+
+if ($type) {
+	# We've seen what looked like a type in a function declaration,
+	# but the function declaration didn't follow.
+	if ($type =~ /^(struct|union)/ and $line eq '{') {
+		complain ("Brace should be one the same line as the '$type' declaration");
+	} else {
+		complain ("Expected a function declaration following '$type', but found something else");
+	}
+	$type = undef;
+}
+
+END {
+	if ($seen_error) {
+		warn "The patch does not validate.\n" if $is_patch;
+		warn "The file does not validate.\n" if $is_file;
+		$? = 1
+	}
+};
+
+=head1 EXAMPLES
+
+=over
+
+=item B<checkpatch.pl hello.c>
+
+Check a single file.
+
+=item B<git diff --cached |checkpatch.pl>
+
+Check the currently staged changes.
+
+=item B<git format-patch -U65535 --stdout -1 |contrib/scripts/checkpatch.pl || :>
+
+A F<.git/hooks/post-commit> oneliner that, wisely, tolerates failures while
+still providing advice. The large line context allows helps checkpatch.pl
+get a better idea about the changes in context of code that does not change.
+
+=back
+
+=head1 BUGS
+
+Proabably too many.
+
+=head1 SEE ALSO
+
+F<CONTRIBUTING>
+
+=head1 COPYRIGHT
+
+Copyright (C) 2018,2021 Red Hat
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+=head1 AUTHOR
+
+Lubomir Rintel C<lkundrak@v3.sk>
+
+=cut
diff --git a/contrib/scripts/code-style-git-post-commit-hook b/contrib/scripts/code-style-git-post-commit-hook
new file mode 100755
index 00000000..8d464842
--- /dev/null
+++ b/contrib/scripts/code-style-git-post-commit-hook
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+set -e
+
+DISABLED=${NM_HOOK_DISABLED:0}
+
+if [ "$DISABLED" == 1 ]; then
+    echo "COMMIT HOOK DISABLED"
+    exit 0
+fi
+
+FORMATTER=contrib/scripts/nm-code-format.sh
+
+# Filter only C source files
+CHANGED_FILES=$(git log --pretty='' --name-only -n1 | grep -E '\.c$|\.h$' | tr '\n' ' ')
+
+echo $CHANGED_FILES
+
+if [ -x "$FORMATTER" ] && [ ! -z "$CHANGED_FILES" ]; then
+    "$FORMATTER" -n "${CHANGED_FILES}"
+fi
diff --git a/contrib/scripts/find-backports b/contrib/scripts/find-backports
new file mode 100755
index 00000000..c2082570
--- /dev/null
+++ b/contrib/scripts/find-backports
@@ -0,0 +1,417 @@
+#!/usr/bin/env python3
+
+import subprocess
+import collections
+import os
+import sys
+import re
+import pprint
+
+
+FNULL = open(os.devnull, "w")
+pp = pprint.PrettyPrinter(indent=4, stream=sys.stderr)
+
+DEBUG = os.environ.get("NM_FIND_BACKPORTS_DEBUG", None) == "1"
+
+
+def dbg_log(s):
+    if DEBUG:
+        print(s, file=sys.stderr)
+
+
+def dbg_pprint(obj):
+    if DEBUG:
+        pp.pprint(obj)
+
+
+def print_err(s):
+    print(s, file=sys.stderr)
+
+
+def die(s):
+    print_err(s)
+    sys.exit(1)
+
+
+def memoize(f):
+    memo = {}
+
+    def helper(x):
+        if x not in memo:
+            memo[x] = f(x)
+        return memo[x]
+
+    return helper
+
+
+def re_bin(r):
+    return r.encode("utf8")
+
+
+def _keys_to_dict(itr):
+    d = collections.OrderedDict()
+    for c in itr:
+        d[c] = None
+    return d
+
+
+@memoize
+def git_ref_exists_full_path(ref):
+    val = git_ref_exists(ref)
+    if val:
+        try:
+            subprocess.check_output(["git", "show-ref", "-q", "--verify", str(ref)])
+        except subprocess.CalledProcessError:
+            pass
+        else:
+            return val
+    return None
+
+
+def _git_ref_exists_eval(ref):
+    try:
+        out = subprocess.check_output(
+            ["git", "rev-parse", "--verify", str(ref) + "^{commit}"],
+            stderr=FNULL,
+        )
+    except subprocess.CalledProcessError:
+        return None
+    o = out.decode("ascii").strip()
+    if len(o) == 40:
+        return o
+    raise Exception(f"git-rev-parse for '{ref}' returned unexpected output {out}")
+
+
+_git_ref_exists_cache = {}
+
+
+def git_ref_exists(ref):
+    val = _git_ref_exists_cache.get(ref, False)
+
+    if val is False:
+        val = _git_ref_exists_eval(ref)
+        _git_ref_exists_cache[ref] = val
+        if val and ref != val:
+            _git_ref_exists_cache[val] = val
+
+    return val
+
+
+@memoize
+def git_get_head_name(ref):
+    out = subprocess.check_output(
+        ["git", "rev-parse", "--symbolic-full-name", str(ref)], stderr=FNULL
+    )
+    return out.decode("utf-8").strip()
+
+
+def git_merge_base(a, b):
+    out = subprocess.check_output(["git", "merge-base", str(a), str(b)], stderr=FNULL)
+    out = out.decode("ascii").strip()
+    assert git_ref_exists(out)
+    return out
+
+
+def git_all_commits_grep(rnge, grep=None):
+    if grep:
+        grep = [("--grep=%s" % g) for g in grep]
+        notes = ["-c", "notes.displayref=refs/notes/bugs"]
+    else:
+        grep = []
+        notes = []
+    out = subprocess.check_output(
+        ["git"]
+        + notes
+        + ["log", "--pretty=%H", "--notes", "--reverse"]
+        + grep
+        + [str(rnge)],
+        stderr=FNULL,
+    )
+    return [x for x in out.decode("ascii").split("\n") if x]
+
+
+def git_logg(commits):
+    commits = list(commits)
+    if not commits:
+        return ""
+    out = subprocess.check_output(
+        [
+            "git",
+            "log",
+            "--no-show-signature",
+            "--no-walk",
+            "--pretty=format:%Cred%h%Creset - %Cgreen(%ci)%Creset [%C(yellow)%an%Creset] %s%C(yellow)%d%Creset",
+            "--abbrev-commit",
+            "--date=local",
+        ]
+        + [str(c) for c in commits],
+        stderr=FNULL,
+    )
+    return out.decode("utf-8").strip()
+
+
+@memoize
+def git_all_commits(rnge):
+    return git_all_commits_grep(rnge)
+
+
+@memoize
+def git_all_commits_set(rnge):
+    return set(git_all_commits_grep(rnge))
+
+
+def git_commit_sorted(commits):
+    commits = list(commits)
+    if not commits:
+        return []
+    out = subprocess.check_output(
+        ["git", "log", "--no-walk", "--pretty=%H", "--reverse"]
+        + [str(x) for x in commits],
+        stderr=FNULL,
+    )
+    out = out.decode("ascii")
+    return [x for x in out.split("\n") if x]
+
+
+@memoize
+def git_ref_commit_body(ref):
+    return subprocess.check_output(
+        [
+            "git",
+            "-c",
+            "notes.displayref=refs/notes/bugs",
+            "log",
+            "-n1",
+            "--pretty=%B%n%N",
+            str(ref),
+        ],
+        stderr=FNULL,
+    )
+
+
+@memoize
+def git_ref_commit_body_get_fixes(ref):
+    body = git_ref_commit_body(ref)
+    result = []
+    for mo in re.finditer(re_bin("\\b[fF]ixes: *([0-9a-z]+)\\b"), body):
+        c = mo.group(1).decode("ascii")
+        h = git_ref_exists(c)
+        if h:
+            result.append(h)
+    if result:
+        # The commit that contains a "Fixes:" line, can also contain an "Ignore-Fixes:" line
+        # to disable it. This only makes sense with refs/notes/bugs notes, to fix up a wrong
+        # annotation.
+        for mo in re.finditer(re_bin("\\bIgnore-[fF]ixes: *([0-9a-z]+)\\b"), body):
+            c = mo.group(1).decode("ascii")
+            h = git_ref_exists(c)
+            try:
+                result.remove(h)
+            except ValueError:
+                pass
+
+    return result
+
+
+@memoize
+def git_ref_commit_body_get_cherry_picked_one(ref):
+    ref = git_ref_exists(ref)
+    if not ref:
+        return None
+    body = git_ref_commit_body(ref)
+    result = None
+    for r in [
+        re_bin("\\(cherry picked from commit ([0-9a-z]+)\\)"),
+        re_bin("\\bIgnore-Backport: *([0-9a-z]+)\\b"),
+    ]:
+        for mo in re.finditer(r, body):
+            c = mo.group(1).decode("ascii")
+            h = git_ref_exists(c)
+            if h:
+                if not result:
+                    result = [h]
+                else:
+                    result.append(h)
+    return result
+
+
+@memoize
+def git_ref_commit_body_get_cherry_picked_recurse(ref):
+    ref = git_ref_exists(ref)
+    if not ref:
+        return None
+
+    def do_recurse(result, ref):
+        result2 = git_ref_commit_body_get_cherry_picked_one(ref)
+        if result2:
+            extra = [h2 for h2 in result2 if h2 not in result]
+            if extra:
+                result.extend(extra)
+                for h2 in extra:
+                    do_recurse(result, h2)
+
+    result = []
+    do_recurse(result, ref)
+    return result
+
+
+def git_commits_annotate_fixes(rnge):
+    commits = git_all_commits(rnge)
+    c_dict = _keys_to_dict(commits)
+    for c in git_all_commits_grep(rnge, grep=["[Ff]ixes:"]):
+        ff = git_ref_commit_body_get_fixes(c)
+        if ff:
+            c_dict[c] = ff
+    return c_dict
+
+
+def git_commits_annotate_cherry_picked(rnge):
+    commits = git_all_commits(rnge)
+    c_dict = _keys_to_dict(commits)
+    for c in git_all_commits_grep(
+        ref_head, grep=["cherry picked from commit", "Ignore-Backport:"]
+    ):
+        ff = git_ref_commit_body_get_cherry_picked_recurse(c)
+        if ff:
+            c_dict[c] = ff
+    return c_dict
+
+
+def git_ref_in_history(ref, rnge):
+    return git_ref_exists(ref) in git_all_commits_set(rnge)
+
+
+if __name__ == "__main__":
+    if len(sys.argv) <= 1:
+        ref_head0 = "HEAD"
+    else:
+        ref_head0 = sys.argv[1]
+
+    ref_head = git_ref_exists(ref_head0)
+    if not ref_head:
+        die('Ref "%s" does not exist' % (ref_head0))
+
+    if not git_ref_exists_full_path("refs/notes/bugs"):
+        die(
+            "Notes refs/notes/bugs not found. Read CONTRIBUTING.md file for how to setup the notes"
+        )
+
+    ref_upstreams = []
+    if len(sys.argv) <= 2:
+        head_name = git_get_head_name(ref_head0)
+        match = False
+        if head_name:
+            match = re.match("^refs/(heads|remotes/[^/]*)/nm-1-([0-9]+)$", head_name)
+        if match:
+            i = int(match.group(2))
+            while True:
+                i += 2
+                r = "nm-1-" + str(i)
+                if not git_ref_exists(r):
+                    r = "refs/remotes/origin/nm-1-" + str(i)
+                    if not git_ref_exists(r):
+                        break
+                ref_upstreams.append(r)
+            ref_upstreams.append("main")
+
+    if not ref_upstreams:
+        if len(sys.argv) <= 2:
+            ref_upstreams = ["main"]
+        else:
+            ref_upstreams = list(sys.argv[2:])
+
+    for h in ref_upstreams:
+        if not git_ref_exists(h):
+            die('Upstream ref "%s" does not exist' % (h))
+
+    print_err("Check %s (%s)" % (ref_head0, ref_head))
+    print_err("Upstream refs: %s" % (ref_upstreams))
+
+    print_err('Check patches of "%s"...' % (ref_head))
+    own_commits_list = git_all_commits(ref_head)
+    own_commits_cherry_picked = git_commits_annotate_cherry_picked(ref_head)
+
+    cherry_picks_all = collections.OrderedDict()
+    for c, cherry_picked in own_commits_cherry_picked.items():
+        if cherry_picked:
+            for c2 in cherry_picked:
+                l = cherry_picks_all.get(c2)
+                if not l:
+                    cherry_picks_all[c2] = [c]
+                else:
+                    l.append(c)
+
+    own_commits_cherry_picked_flat = set()
+    for c, p in own_commits_cherry_picked.items():
+        own_commits_cherry_picked_flat.add(c)
+        if p:
+            own_commits_cherry_picked_flat.update(p)
+
+    dbg_log(">>> own_commits_cherry_picked")
+    dbg_pprint(own_commits_cherry_picked)
+
+    dbg_log(">>> cherry_picks_all")
+    dbg_pprint(cherry_picks_all)
+
+    # find all commits on the upstream branches that fix another commit.
+    fixing_commits = {}
+    for ref_upstream in ref_upstreams:
+        ref_str = ref_head + ".." + ref_upstream
+        print_err(f'Check upstream patches "{ref_str}"...')
+        for c, fixes in git_commits_annotate_fixes(ref_str).items():
+            if not fixes:
+                dbg_log(f">>> test {c} : SKIP (does not fix anything)")
+                continue
+            if c in cherry_picks_all:
+                # commit 'c' is already backported. Skip it.
+                dbg_log(f">>> test {c} => {fixes} : SKIP (already backported)")
+                continue
+            dbg_log(f">>> test {c} => {fixes} : process")
+            for f in fixes:
+                if f not in own_commits_cherry_picked_flat:
+                    # commit "c" fixes commit "f", but this is not one of our own commits
+                    # and not interesting.
+                    dbg_log(f">>> fixes {f} not in own_commits_cherry_picked")
+                    continue
+                dbg_log(f">>> take {c} (fixes {fixes})")
+                fixing_commits[c] = fixes
+                break
+
+    extra = collections.OrderedDict(
+        [(c, git_ref_commit_body_get_cherry_picked_recurse(c)) for c in fixing_commits]
+    )
+    extra2 = []
+    for c in extra:
+        is_back = False
+        for e_v in extra.values():
+            if c in e_v:
+                is_back = True
+                break
+        if not is_back:
+            extra2.append(c)
+
+    commits_good = extra2
+
+    commits_good = git_commit_sorted(commits_good)
+
+    print_err(git_logg(commits_good))
+
+    not_in = [
+        c
+        for c in commits_good
+        if not git_ref_in_history(c, f"{ref_head}..{ref_upstreams[0]}")
+    ]
+    if not_in:
+        print_err("")
+        print_err(
+            f'WARNING: The following commits are not from the first reference "{ref_upstreams[0]}".'
+        )
+        print_err(
+            f'  You may want to first backports those patches to "{ref_upstreams[0]}".'
+        )
+        for l in git_logg(git_commit_sorted(not_in)).splitlines():
+            print_err(f"  - {l}")
+        print_err("")
+
+    for c in reversed(commits_good):
+        print("%s" % (c))
diff --git a/contrib/scripts/git-backport-merge b/contrib/scripts/git-backport-merge
new file mode 100755
index 00000000..8ba2244f
--- /dev/null
+++ b/contrib/scripts/git-backport-merge
@@ -0,0 +1,58 @@
+#!/bin/bash
+
+# Uses `git cherry-pick -x` to backport a merge commit to an older branch.
+#
+# Usage:
+#   First checkout the old-stable branch, that is the target for the backport.
+#   Then `git-backport-merge MERGE_REF [REFS...]`
+#     MERGE_REF is the merge commit that should be backported.
+#     [REFS...] is the commits that should be backported. If omitted,
+#       it automatically takes the parent commits of the merge commit.
+
+die() {
+    printf '%s\n' "$*" >&2
+    exit 1
+}
+
+backport_merge() {
+    test "$#" -gt 0 || die "Requires the commit ref to backport (and optimally select the commits to include)"
+
+    local M="${@:$#}"
+    local h
+
+    if test "$#" -eq 1; then
+        local C=($(git log --reverse "--pretty=%H" "$M"^1.."$M"^2))
+    else
+        local C=("${@:1:$#-1}")
+    fi
+
+    local OLD_HEAD="$(git rev-parse HEAD)" || die "failed to get current HEAD"
+
+    test -n "$(git status --porcelain --untracked-files=no)" && die "Working directory contains changes. Abort."
+
+    local M_ID="$(git rev-parse "$M"^{commit})" || die "\"$M\" is not a valid commit"
+
+    trap EXIT 'test -z "$OLD_HEAD" || git reset "$OLD_HEAD" --hard'
+
+    for h in "${C[@]}"; do
+        if ! git cherry-pick --allow-empty -x "$h" ; then
+            git cherry-pick --abort
+            die "failed to cherry-pick commit \"$h\" on top of \"$(git rev-parse HEAD)\""
+        fi
+    done
+
+    local NEW_HEAD="$(git rev-parse HEAD)" || die "failed to get new HEAD"
+
+    git reset --hard "$OLD_HEAD" || die "Failed to reset to previous HEAD \"$OLD_HEAD\""
+
+    git merge --no-ff --no-edit "$NEW_HEAD" || die "Failed to merge old HEAD \"$OLD_HEAD\" with new \"$NEW_HEAD\""
+
+    git commit --amend --allow-empty -C "$M" || die "Failed to amend merge commit \"$(git rev-parse HEAD)\" with commit message from \"$M\""
+
+    git rev-parse "$M" | sed 's/.*/(cherry picked from commit \0)/' | GIT_EDITOR='sh -c "cat >> \"$1\""' git commit --allow-empty --amend || \
+         die "Failed to amend merge commit \"$(git rev-parse HEAD)\" with cherry-picked-from message from \"$M\""
+
+    OLD_HEAD=
+}
+
+backport_merge "$@"
diff --git a/contrib/scripts/git-subtree-reimport.sh b/contrib/scripts/git-subtree-reimport.sh
new file mode 100755
index 00000000..73319044
--- /dev/null
+++ b/contrib/scripts/git-subtree-reimport.sh
@@ -0,0 +1,63 @@
+#!/bin/bash
+
+# In our git repository we vendor in several external projects.
+# We do so via git-subtree.
+#
+# Run this script (without arguments) for re-importing the latest
+# version of those projects.
+#
+# You can also specify the projects to reimport on the command line,
+# ./contrib/scripts/git-subtree-reimport.sh  [ c-list | c-rbtree | c-siphash | c-stdaux | n-acd | n-dhcp4 ... ]
+
+set -e
+
+cd "$(dirname "$(readlink -f "$0")")/../.."
+
+reimport() {
+    local d="$1"
+    local project
+    local branch
+
+    if [[ "$d" = c-* ]] ; then
+        project=c-util
+        branch=main
+    else
+        project=nettools
+        branch=master
+    fi
+
+    CMD=( git subtree pull --prefix "src/$d" "git@github.com:$project/$d.git" "$branch" --squash -m \
+"$d: re-import git-subtree for 'src/$d'
+
+  git subtree pull --prefix src/$d git@github.com:$project/$d.git $branch --squash
+" )
+
+    printf '\n>>>> %s >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n' "$d"
+    printf '>>>'
+    for c in "${CMD[@]}"; do
+        printf ' %q' "$c"
+    done
+    printf '\n'
+
+    "${CMD[@]}" 2>&1
+
+    local REMOTE_COMMIT="$(git rev-parse FETCH_HEAD)"
+
+    echo ">>>>> RESULT:"
+    printf ">>> git diff %s: HEAD:src/%s\n" "$REMOTE_COMMIT" "$d"
+    GIT_PAGER=cat git diff --color=always "$REMOTE_COMMIT:" "HEAD:src/$d"
+}
+
+reimport_all() {
+    local ARGS
+
+    ARGS=( "$@" )
+    if [ "${#ARGS[@]}" = 0 ]; then
+        ARGS=( c-list c-rbtree c-siphash c-stdaux n-acd n-dhcp4 )
+    fi
+    for d in "${ARGS[@]}" ; do
+        reimport "$d"
+    done
+}
+
+reimport_all "$@"
diff --git a/contrib/scripts/modemu.pl b/contrib/scripts/modemu.pl
new file mode 100755
index 00000000..0e90fafa
--- /dev/null
+++ b/contrib/scripts/modemu.pl
@@ -0,0 +1,299 @@
+#!/usr/bin/env perl
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# Copyright (C) 2018 Red Hat, Inc.
+#
+
+# $ perldoc modemu.pl for eye-pleasing view of the manual:
+
+=head1 NAME
+
+modemu.pl - emulate a serial modem
+
+=head1 SYNOPSIS
+
+modemu.pl [<name>] [-- <pppd> ...]
+
+=head1 DESCRIPTION
+
+B<modemu.pl> opens a PTY, links the slave side to F</dev> and announces a
+fake kobject via netlink as if it were a real serial device, so that
+ModemManager picks it up.
+
+Then it answers to a very basic subset of AT commands, sufficient making
+ModemManager recognize it as a 3GPP capable modem registered to a network.
+
+Upon receiving the dial (ATD) command, it spawns C<pppd> so that
+NetworkManager can establish a connection.
+
+B<modemu.pl> needs superuser privileges to be able to announce a kobject
+and create a F</dev> node.
+
+=head1 OPTIONS
+
+=over 4
+
+=item B<< <name> >>
+
+Create a modem of given name. Links it to F<< /dev/<name> >>.
+
+Defaults to I<modemu>.
+
+=item B<< <pppd> >>
+
+Specifies extra arguments to be prepended before C<pppd> to the default
+set of I<nodetach notty local logfd 2 nopersist>.
+
+Defaults to I<pppd dump debug 172.31.82.1:172.31.82.2>.
+
+=back
+
+=cut
+
+use strict;
+use warnings;
+
+use Errno;
+use Socket;
+use IO::Pty;
+use IO::Handle;
+
+use constant AF_NETLINK => 16;
+use constant NETLINK_KOBJECT_UEVENT => 15;
+
+# This allows us to use buffered read for lines from ModemManager
+# despite not ending with \n
+IO::Handle->input_record_separator ("\r");
+
+# Parse command line arguments
+my $name;
+my @pppd = qw/pppd dump debug 172.31.82.1:172.31.82.2/;
+while (@ARGV) {
+	$_ = shift @ARGV;
+	if ($_ eq '--') {
+		@pppd = @ARGV;
+		last;
+	} else {
+		die "Extra argument: '$_'" if $name;
+		$name = $_;
+	}
+};
+$name ||= 'modemu';
+
+socket my $fd, AF_NETLINK, SOCK_RAW, NETLINK_KOBJECT_UEVENT
+	or die "Can't create a netlink socket: $!";
+
+my $seqnum = 666;
+sub send_netlink
+{
+	my %props = (@_, SEQNUM => $seqnum++);
+	my $props = join '', map { $_, '=', $props{$_}, "\0" } keys %props;
+
+	my $head = pack 'a8NLLLNLLL',
+		# signature + magic
+		'libudev',
+		0xfeedcafe,
+
+		# 40 octets is the length of this header
+		40, 40, 40 + length ($props),
+
+		# Digest::MurmurHash2::Neutral::murmur_hash2_neutral("tty")
+		0x8afa90c8,
+
+		0x00000000,
+		0x00040002,
+		0x00008010;
+
+	$! = undef;
+	send $fd, "$head$props", 0, pack 'SSLL', AF_NETLINK, 0, 0, 0x0002;
+	# RHEL 7 kernel responds ECONNREFUSED even thoguh the sendto succeeded. Weird.
+	die "Can't send a netlink message: $!" if $! and not $!{ECONNREFUSED};
+}
+
+my $devpath = '/devices/pci0000:00/0000:00:00.0';
+unless (-d "/sys/$devpath") {
+	# Create a virtual device. Older ModemManager likes a hotpluggable bus
+	# (USB, PCI), but there's none on an IBM POWER lpar...
+	warn "No PCI bus to use for parent. Don't expect this to work with ModemManager 1.6";
+	$devpath = '/devices/virtual';
+}
+
+my %props = (
+	DEVPATH			=> "$devpath/$name",
+	SUBSYSTEM		=> 'tty',
+	DEVNAME			=> "/dev/$name",
+
+	# Whitelisting that works for both ModemManager 1.6 and 1.8
+	ID_MM_CANDIDATE		=> '1',
+	ID_MM_DEVICE_PROCESS	=> '1',
+);
+
+sub cleanup
+{
+	unlink "/dev/$name";
+	send_netlink (ACTION => 'remove', %props) if $fd;
+}
+
+# Ensure we clean up before and after.
+END { cleanup };
+$SIG{INT} = sub { cleanup; die };
+$SIG{TERM} = sub { cleanup; die };
+cleanup;
+
+my $pty = new IO::Pty;
+my $ptyname = ttyname $pty;
+symlink $ptyname, "/dev/$name" or die "Can't create /dev/$name: $!";
+send_netlink (ACTION => 'add', %props);
+my ($pdptype, $apn);
+
+# Here's a good refernce of AT command a modern-ish modem probably uses:
+# https://infocenter.nordicsemi.com/index.jsp?topic=%2Fref_at_commands%2FREF%2Fat_commands%2Fpacket_domain%2Fcgact_set.html
+
+while (<$pty>) {
+	chomp;
+
+	if (/^AT$/ or /^ATE0$/ or /^ATV1$/ or /^AT\+CMEE=1$/ or /^ATX4$/ or /^AT&C1$/ or /^ATZ$/) {
+		# Standard Hayes commands that are basically used to
+		# ensure the modem is in a known state. Accept them all.
+		print $pty "\r\n";
+		print $pty "OK\r\n";
+
+	} elsif (/^AT\+CPIN\?$/) {
+		# PIN unlocked. Required.
+		print $pty "\r\n";
+		print $pty "+CPIN:READY\r\n";
+		print $pty "\r\n";
+		print $pty "OK\r\n";
+
+	} elsif (/^AT\+COPS=0$/) {
+		# Select access technology (we just accept 0=automatic)
+		print $pty "\r\n";
+		print $pty "OK\r\n";
+
+	} elsif (/^AT\+CGREG\?$/) {
+		# 3GPP Registration status.
+		print $pty "\r\n";
+		print $pty "+CGREG: 0,1\r\n";
+		print $pty "\r\n";
+		print $pty "OK\r\n";
+
+	# The PDP (packet data protocol/profile?) context handling below is very
+	# rudimentary: just enough to keep ModemManager 1.18 happy. It basically
+	# just starts with no contexts at all and then expects MM to set and
+	# activate profile number 1.
+
+	} elsif (/^AT\+CGDCONT=\?$/) {
+		# Get supported PDP contexts
+		print $pty "\r\n";
+		print $pty "+CGDCONT: (1-10),(\"IP\"),,,(0-1),(0-1)\r\n";
+		print $pty "+CGDCONT: (1-10),(\"IPV6\"),,,(0-1),(0-1)\r\n";
+		print $pty "OK\r\n";
+
+	} elsif (/^AT\+CGDCONT=1,"(.*)","(.*)"$/) {
+		# Create the PDP context. Remember it, MM is going to check it later
+		($pdptype, $apn) = ($1, $2);
+		print $pty "\r\n";
+		print $pty "OK\r\n";
+
+
+	} elsif (/^AT\+CGDCONT\?$/) {
+		# List the PDP context we're aware of.
+		print $pty "\r\n";
+		print $pty "+CGDCONT: 1,\"$pdptype\",\"$apn\",\"0.0.0.0\",0,0,0,0\r\n"
+			if defined $pdptype;
+		print $pty "OK\r\n";
+
+	} elsif (/^AT\+CGACT\?$/) {
+		# List available PDP contexts with states: profile 1 state 0 (inactive)
+		print $pty "\r\n";
+		print $pty "+CGACT: 0,1\r\n";
+		print $pty "OK\r\n";
+
+	} elsif (/^AT\+CGACT=0,1$/) {
+		# Deactivate a PDP context
+		print $pty "\r\n";
+		print $pty "OK\r\n";
+
+	} elsif (/^AT\+COPS\?$/) {
+		# Current operators
+		# Not strictly required, but allows NetworkManager to just connect
+		# the modem device without explicitly setting an APN
+		print $pty "\r\n";
+		print $pty "+COPS: 0,2,\"65302\",7\r\n"; # MCCMNC
+		print $pty "OK\r\n";
+
+	} elsif (/^ATD/) {
+		print $pty "\r\n";
+		print $pty "CONNECT 28800000\r\n";
+
+		my $ppp = fork;
+		die "Can't fork: $!" unless defined $ppp;
+		if ($ppp == 0) {
+			close STDIN;
+			close STDOUT;
+			open STDIN, '<&', $pty or die "Can't dup pty to a pppd stdin: $!";
+			open STDOUT, '>&', $pty or die "Can't dup pty to a pppd stdout: $!";
+			close $pty;
+			exec @pppd, qw/nodetach notty local logfd 2 nopersist/;
+			die "Can't exec pppd: $!";
+		}
+		waitpid $ppp, 0;
+	} else {
+		print $pty "\r\n";
+		print $pty "ERROR\r\n";
+	}
+}
+
+=head1 EXAMPLES
+
+=over
+
+=item B<modemu.pl>
+
+Just create a modem named I<modemu>, with the default PPP arguments.
+
+=item B<modemu.pl ttyS666>
+
+Same as above, just name the modem I<ttyS666>.
+
+=item B<modemu.pl -- unshare --net pppd 172.31.82.1:172.31.82.2>
+
+Avoid polluting the namespace with the modem end of PPP connection.
+
+=item B<modemu.pl -- pppd 10.0.0.1:10.0.0.2>
+
+Override the C<pppd> parameters: no debug logging and different set of
+addresses.
+
+=item B<modemu.pl mymodem -- pppd 10.0.0.1:10.0.0.2>
+
+Same as above, with a modem name different from default.
+
+=back
+
+=head1 BUGS
+
+Only works on machines with a PCI bus. ModemManager is picky about platform
+devices and accepts PCI and USB buses easily. Which is why pretent to have
+our tty on the PCI root device.
+
+Terminates after a single PPP session. C<pppd> seems to hang up the PTY.
+
+=head1 SEE ALSO
+
+L<ModemManager(8)>, L<pppd(8)>
+
+=head1 COPYRIGHT
+
+Copyright (C) 2018 Lubomir Rintel
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+=head1 AUTHOR
+
+Lubomir Rintel C<lkundrak@v3.sk>
+
+=cut
diff --git a/contrib/scripts/nm-ci-patch-gtkdoc.sh b/contrib/scripts/nm-ci-patch-gtkdoc.sh
new file mode 100755
index 00000000..e72a0a8f
--- /dev/null
+++ b/contrib/scripts/nm-ci-patch-gtkdoc.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+
+# patch gtk-doc for https://gitlab.gnome.org/GNOME/gtk-doc/merge_requests/2
+
+cd /
+
+patch -f -p 1 --fuzz 0 --reject-file=- <<EOF
+diff --git a/usr/share/gtk-doc/python/gtkdoc/scan.py b/usr/share/gtk-doc/python/gtkdoc/scan.py
+index f1f167235ab2e4c62676fbcfb87ebbe55c95b944..b59dd17abfa5f42b7bb06d239f9c78e5efffbf5d 100644
+--- a/usr/share/gtk-doc/python/gtkdoc/scan.py
++++ b/usr/share/gtk-doc/python/gtkdoc/scan.py
+@@ -427,20 +427,26 @@ def ScanHeader(input_file, section_list, decl_list, get_types, options):
+             elif m9:
+                 # We've found a 'typedef struct _<name> <name>;'
+                 # This could be an opaque data structure, so we output an
+                 # empty declaration. If the structure is actually found that
+                 # will override this.
+                 structsym = m9.group(1).upper()
+                 logging.info('%s typedef: "%s"', structsym, m9.group(2))
+                 forward_decls[m9.group(2)] = '<%s>\n<NAME>%s</NAME>\n%s</%s>\n' % (
+                     structsym, m9.group(2), deprecated, structsym)
+ 
++                bm = re.search(r'^(\S+)(Class|Iface|Interface)\b', m9.group(2))
++                if bm:
++                    objectname = bm.group(1)
++                    logging.info('Found object: "%s"', objectname)
++                    title = '<TITLE>%s</TITLE>' % objectname
++
+             elif re.search(r'^\s*(?:struct|union)\s+_(\w+)\s*;', line):
+                 # Skip private structs/unions.
+                 logging.info('private struct/union')
+ 
+             elif m10:
+                 # Do a similar thing for normal structs as for typedefs above.
+                 # But we output the declaration as well in this case, so we
+                 # can differentiate it from a typedef.
+                 structsym = m10.group(1).upper()
+                 logging.info('%s:%s', structsym, m10.group(2))
+EOF
+
diff --git a/contrib/scripts/nm-ci-run.sh b/contrib/scripts/nm-ci-run.sh
new file mode 100755
index 00000000..e385ef1e
--- /dev/null
+++ b/contrib/scripts/nm-ci-run.sh
@@ -0,0 +1,299 @@
+#!/bin/bash
+
+# Arguments via environment variables:
+#  - CI
+#  - CC
+#  - BUILD_TYPE
+#  - CFLAGS
+#  - WITH_DOCS
+
+set -ex
+
+die() {
+    printf "%s\n" "$@"
+    exit 1
+}
+
+_is_true() {
+    case "$1" in
+        1|y|yes|YES|Yes|on)
+            return 0
+            ;;
+        0|n|no|NO|No|off)
+            return 1
+            ;;
+        "")
+            if [ "$2" == "" ]; then
+                die "not a boolean argument \"$1\""
+            fi
+            _is_true "$2"
+            return $?
+            ;;
+        *)
+            die "not a boolean argument \"$1\""
+            ;;
+    esac
+}
+
+USE_CCACHE=0
+if command -v ccache &>/dev/null; then
+    USE_CCACHE=1
+    export PATH="/usr/lib64/ccache:/usr/lib/ccache${PATH:+:${PATH}}"
+fi
+
+IS_FEDORA=0
+IS_CENTOS=0
+IS_ALPINE=0
+grep -q '^NAME=.*\(CentOS\)' /etc/os-release && IS_CENTOS=1
+grep -q '^NAME=.*\(Fedora\)' /etc/os-release && IS_FEDORA=1
+grep -q '^NAME=.*\(Alpine\)' /etc/os-release && IS_ALPINE=1
+
+###############################################################################
+
+if [ "$BUILD_TYPE" == meson ]; then
+    _TRUE=true
+    _FALSE=false
+elif [ "$BUILD_TYPE" == autotools ]; then
+    _TRUE=yes
+    _FALSE=no
+else
+    die "invalid \$BUILD_TYPE \"$BUILD_TYPE\""
+fi
+
+_WITH_CRYPTO="gnutls"
+_WITH_WERROR=1
+_WITH_LIBTEAM="$_TRUE"
+_WITH_DOCS="$_TRUE"
+_WITH_SYSTEMD_LOGIND="$_TRUE"
+if [ $IS_ALPINE = 1 ]; then
+    _WITH_SYSTEMD_LOGIND="$_FALSE"
+fi
+
+if [ -z "${NMTST_SEED_RAND+x}" ]; then
+    NMTST_SEED_RAND="$SRANDOM"
+    if [ -z "$NMTST_SEED_RAND" ]; then
+        NMTST_SEED_RAND="$(( ( (RANDOM<<15|RANDOM)<<15|RANDOM ) % 0xfffffffe ))"
+    fi
+fi
+export NMTST_SEED_RAND
+
+case "$CI" in
+    ""|"true"|"default"|"gitlab")
+        CI=default
+        ;;
+    *)
+        die "invalid \$CI \"$CI\""
+        ;;
+esac
+
+if [ "$CC" != gcc ]; then
+    _WITH_CRYPTO=nss
+fi
+
+if [ "$WITH_DOCS" != "" ]; then
+    if _is_true "$WITH_DOCS"; then
+        _WITH_DOCS="$_TRUE"
+    else
+        _WITH_DOCS="$_FALSE"
+    fi
+fi
+
+unset _WITH_VALGRIND_CHECKED
+_with_valgrind() {
+    _is_true "$WITH_VALGRIND" 0 || return 1
+
+    test "$_WITH_VALGRIND_CHECKED" = "1" && return 0
+    _WITH_VALGRIND_CHECKED=1
+
+    if [ "$IS_ALPINE" = 1 ]; then
+        # on Alpine we have no debug symbols and the suppressions
+        # don't work. Skip valgrind tests.
+        WITH_VALGRIND=0
+    fi
+
+    # Certain glib2 versions are known to report *lots* of leaks. Disable
+    # valgrind tests in this case.
+    # https://bugzilla.redhat.com/show_bug.cgi?id=1710417
+    if grep -q '^PRETTY_NAME="Fedora 30 (.*)"$' /etc/os-release ; then
+        if rpm -q glib2 | grep -q glib2-2.60.2-1.fc30 ; then
+            WITH_VALGRIND=0
+        fi
+    elif grep -q '^PRETTY_NAME="Fedora 31 (.*)"$' /etc/os-release; then
+        if rpm -q glib2 | grep -q glib2-2.61.0-2.fc31 ; then
+            WITH_VALGRIND=0
+        fi
+    elif grep -q '^PRETTY_NAME="Debian.*sid"$' /etc/os-release; then
+        if dpkg -s libglib2.0-bin | grep -q '^Version: 2.66.4-2$' ; then
+            WITH_VALGRIND=0
+        fi
+    fi
+    if [ "$WITH_VALGRIND" == 0 ]; then
+        echo "Don't use valgrind due to known issues in other packages."
+        return 1
+    fi
+    return 0
+}
+
+###############################################################################
+
+_print_test_logs() {
+    echo ">>>> PRINT TEST LOGS $1 (start)"
+    if test -f test-suite.log; then
+        cat test-suite.log
+    fi
+    echo ">>>> PRINT TEST LOGS $1 (done)"
+    if _with_valgrind; then
+        echo ">>>> PRINT VALGRIND LOGS $1 (start)"
+        find -name '*.valgrind-log' -print0 | xargs -0 grep -H ^ || true
+        echo ">>>> PRINT VALGRIND LOGS $1 (done)"
+    fi
+}
+
+run_autotools() {
+    NOCONFIGURE=1 ./autogen.sh
+    mkdir ./build
+    if [ "$_WITH_WERROR" == 1 ]; then
+        _WITH_WERROR_VAL="error"
+    else
+        _WITH_WERROR_VAL="yes"
+    fi
+    DISABLE_DEPENDENCY_TRACKING=
+    if [ $IS_ALPINE = 1 ]; then
+        DISABLE_DEPENDENCY_TRACKING='--disable-dependency-tracking'
+    fi
+    pushd ./build
+        ../configure \
+            --prefix="$PWD/INST" \
+            $DISABLE_DEPENDENCY_TRACKING \
+            \
+            --enable-introspection=$_WITH_DOCS \
+            --enable-gtk-doc=$_WITH_DOCS \
+            --with-systemd-logind=$_WITH_SYSTEMD_LOGIND \
+            --enable-more-warnings="$_WITH_WERROR_VAL" \
+            --enable-tests=yes \
+            --with-crypto=$_WITH_CRYPTO \
+            \
+            --with-ebpf=no \
+            \
+            --with-iwd=yes \
+            --with-ofono=yes \
+            --enable-teamdctl=$_WITH_LIBTEAM \
+            \
+            --with-dhcpcanon=yes \
+            --with-dhcpcd=yes \
+            --with-dhclient=yes \
+            \
+            --with-netconfig=/bin/nowhere/netconfig \
+            --with-resolvconf=/bin/nowhere/resolvconf \
+            \
+            --enable-ifcfg-rh=yes \
+            --enable-ifupdown=yes \
+            \
+            #end
+
+        if [ "$CONFIGURE_ONLY" != 1 ]; then
+            make -j 6
+            make install
+
+            export NM_TEST_CLIENT_CHECK_L10N=1
+
+            if ! make check -j 6 -k ; then
+                _print_test_logs "first-test"
+                echo ">>>> RUN SECOND TEST (start)"
+                NMTST_DEBUG="debug,TRACE,no-expect-message" make check -k || :
+                echo ">>>> RUN SECOND TEST (done)"
+                _print_test_logs "second-test"
+                die "autotools test failed"
+            fi
+
+            if _with_valgrind; then
+                if ! NMTST_USE_VALGRIND=1 make check -j 3 -k ; then
+                    _print_test_logs "(valgrind test)"
+                    die "autotools+valgrind test failed"
+                fi
+            fi
+        fi
+    popd
+}
+
+###############################################################################
+
+run_meson() {
+    if [ "$_WITH_WERROR" == 1 ]; then
+        _WITH_WERROR_VAL="--werror"
+    else
+        _WITH_WERROR_VAL=""
+    fi
+    meson setup build \
+        \
+        -Dprefix="$PWD/INST" \
+        \
+        --warnlevel 2 \
+        $_WITH_WERROR_VAL \
+        \
+        -D ld_gc=false \
+        -D session_tracking=no \
+        -D systemdsystemunitdir=no \
+        -D systemd_journal=false \
+        -D selinux=false \
+        -D libaudit=no \
+        -D libpsl=false \
+        -D vapi=false \
+        -D introspection=$_WITH_DOCS \
+        -D qt=false \
+        -D crypto=$_WITH_CRYPTO \
+        -D docs=$_WITH_DOCS \
+        \
+        -D ebpf=false \
+        \
+        -D iwd=true \
+        -D ofono=true \
+        -D teamdctl=$_WITH_LIBTEAM \
+        \
+        -D dhclient=/bin/nowhere/dhclient \
+        -D dhcpcanon=/bin/nowhere/dhcpcanon \
+        -D dhcpcd=/bin/nowhere/dhcpd \
+        \
+        -D netconfig=/bin/nowhere/netconfig \
+        -D resolvconf=/bin/nowhere/resolvconf \
+        \
+        -D ifcfg_rh=false \
+        -D ifupdown=true \
+        \
+        #end
+
+    export NM_TEST_CLIENT_CHECK_L10N=1
+
+    if [ "$CONFIGURE_ONLY" != 1 ]; then
+        ninja -C build -v
+        ninja -C build install
+
+        if ! meson test -C build -v --print-errorlogs ; then
+            echo ">>>> RUN SECOND TEST (start)"
+            NMTST_DEBUG="debug,TRACE,no-expect-message" \
+            meson test -C build -v --print-errorlogs || :
+            echo ">>>> RUN SECOND TEST (done)"
+            die "meson test failed"
+        fi
+
+        if _with_valgrind; then
+            if ! NMTST_USE_VALGRIND=1 meson test -C build -v --print-errorlogs ; then
+                _print_test_logs "(valgrind test)"
+                die "meson+valgrind test failed"
+            fi
+        fi
+    fi
+}
+
+###############################################################################
+
+if [ "$BUILD_TYPE" == autotools ]; then
+    run_autotools
+elif [ "$BUILD_TYPE" == meson ]; then
+    run_meson
+fi
+
+if [ "$USE_CCACHE" = 1 ]; then
+    echo "ccache statistics:"
+    ccache -s
+fi
diff --git a/contrib/scripts/nm-code-format-container.sh b/contrib/scripts/nm-code-format-container.sh
new file mode 100755
index 00000000..7a5ce0d4
--- /dev/null
+++ b/contrib/scripts/nm-code-format-container.sh
@@ -0,0 +1,46 @@
+#!/bin/bash
+
+set -e
+
+die() {
+    echo "$@" >&2
+    exit 1
+}
+
+DIR="$(realpath "$(dirname "$0")/../../")"
+cd "$DIR"
+
+# The correct clang-format version is the one from the Fedora version used in our
+# gitlab-ci pipeline. Parse it from ".gitlab-ci/config.yml".
+FEDORA_VERSION="$(sed '/^    tier: 1/,/^  - name/!d' .gitlab-ci/config.yml | sed -n "s/^      - '\([0-9]\+\)'$/\1/p" | sed -n 1p)"
+
+test -n "$FEDORA_VERSION" || die "Could not detect the Fedora version in .gitlab-ci/config.yml"
+
+IMAGENAME="nm-code-format:f$FEDORA_VERSION"
+
+ARGS=( "$@" )
+
+if ! podman image exists "$IMAGENAME" ; then
+    echo "Building image \"$IMAGENAME\"..."
+    podman build \
+        --squash-all \
+        --tag "$IMAGENAME" \
+        -f <(cat <<EOF
+FROM fedora:$FEDORA_VERSION
+RUN dnf upgrade -y
+RUN dnf install -y git /usr/bin/clang-format
+EOF
+)
+fi
+
+CMD=( ./contrib/scripts/nm-code-format.sh "${ARGS[@]}" )
+
+podman run \
+    --rm \
+    --name "nm-code-format-f$FEDORA_VERSION" \
+    -v "$DIR:/tmp/NetworkManager:Z" \
+    -w /tmp/NetworkManager \
+    -e "_NM_CODE_FORMAT_CONTAINER=$IMAGENAME" \
+    -ti \
+    "$IMAGENAME" \
+    "${CMD[@]}"
diff --git a/contrib/scripts/nm-code-format.sh b/contrib/scripts/nm-code-format.sh
new file mode 100755
index 00000000..3c18cd77
--- /dev/null
+++ b/contrib/scripts/nm-code-format.sh
@@ -0,0 +1,222 @@
+#!/bin/bash
+
+set -e
+
+die() {
+    printf '%s\n' "$*" >&2
+    exit 1
+}
+
+EXCLUDE_PATHS_TOPLEVEL=(
+    "src/c-list"
+    "src/c-rbtree"
+    "src/c-siphash"
+    "src/c-stdaux"
+    "src/libnm-std-aux/unaligned-fundamental.h"
+    "src/libnm-std-aux/unaligned.h"
+    "src/libnm-systemd-core/src"
+    "src/libnm-systemd-shared/src"
+    "src/linux-headers"
+    "src/n-acd"
+    "src/n-dhcp4"
+)
+
+NM_ROOT="$(git rev-parse --show-toplevel)" || die "not inside a git repository"
+NM_PREFIX="$(git rev-parse --show-prefix)" || die "not inside a git repository"
+
+if [ ! -f "$NM_ROOT/.clang-format" ]; then
+    die "Error: the clang-format file in \"$NM_ROOT\" does not exist"
+fi
+
+if ! command -v clang-format &> /dev/null; then
+    die "Error: clang-format is not installed. On RHEL/Fedora/CentOS run 'dnf install clang-tools-extra'"
+fi
+
+if test -n "$NM_PREFIX"; then
+    EXCLUDE_PATHS=()
+    for e in "${EXCLUDE_PATHS_TOPLEVEL[@]}"; do
+        REGEX="^$NM_PREFIX([^/].*)$"
+        if [[ "$e" =~ $REGEX ]]; then
+            EXCLUDE_PATHS+=("${BASH_REMATCH[1]}")
+        fi
+    done
+else
+    EXCLUDE_PATHS=("${EXCLUDE_PATHS_TOPLEVEL[@]}")
+fi
+
+FILES=()
+HAS_EXPLICIT_FILES=0
+SHOW_FILENAMES=0
+TEST_ONLY=0
+CHECK_UPSTREAM=
+
+usage() {
+    printf "Usage: %s [OPTION]... [FILE]...\n" "$(basename "$0")"
+    printf "Reformat source files using NetworkManager's code-style.\n\n"
+    printf "If no file is given the script runs on the whole codebase.\n"
+    printf "OPTIONS:\n"
+    printf "    -h                 Print this help message.\n"
+    printf "    -i                 Reformat files (the default).\n"
+    printf "    -n|--dry-run       Only check the files (contrary to \"-i\").\n"
+    printf "    -a|--all           Check all files (the default).\n"
+    printf "    -u|--upstream COMMIT Check only files from \`git diff --name-only COMMIT\` (contrary to \"-a\").\n"
+    printf "                       This also affects directories given in the [FILE] list, but not files.\n"
+    printf "                       If this is the last parameter and COMMIT is unspecified/empty, it defaults to \"main\".\n"
+    printf "    -F|--fast          Same as \`-u HEAD^\`.\n"
+    printf "    --show-filenames   Only print the filenames that would be checked/formatted\n"
+    printf "    --                 Separate options from filenames/directories\n"
+    if [ -n "${_NM_CODE_FORMAT_CONTAINER+x}" ] ; then
+        printf "\n"
+        printf "Command runs inside container image \"$_NM_CODE_FORMAT_CONTAINER\".\n"
+        printf "Delete/renew image with \`podman rmi \"$_NM_CODE_FORMAT_CONTAINER\"\`.\n"
+    fi
+}
+
+ls_files_exist() {
+    local OLD_IFS="$IFS"
+    local f
+
+    IFS=$'\n'
+    for f in $(cat) ; do
+        test -f "$f" && printf '%s\n' "$f"
+    done
+    IFS="$OLD_IFS"
+}
+
+ls_files_filter() {
+    local OLD_IFS="$IFS"
+    local f
+
+    IFS=$'\n'
+    for f in $(cat) ; do
+        local found=1
+        local p
+        for p; do
+            [[ "$f" = "$p/"* ]] && found=
+            [[ "$f" = "$p" ]] && found=
+        done
+        test -n "$found" && printf '%s\n' "$f"
+    done
+    IFS="$OLD_IFS"
+}
+
+g_ls_files() {
+    local pattern="$1"
+    shift
+
+    if [ -z "$CHECK_UPSTREAM" ]; then
+        git ls-files -- "$pattern"
+    else
+        git diff --no-renames --name-only "$CHECK_UPSTREAM" -- "$pattern" \
+            | ls_files_exist
+    fi | ls_files_filter "$@"
+}
+
+HAD_DASHDASH=0
+while (( $# )); do
+    if [ "$HAD_DASHDASH" = 0 ]; then
+        case "$1" in
+            -h)
+                usage
+                exit 0
+                ;;
+            --show-filenames)
+                SHOW_FILENAMES=1
+                shift
+                continue
+                ;;
+            -a|--all)
+                CHECK_UPSTREAM=
+                shift
+                continue
+                ;;
+            -u|--upstream)
+                shift
+                CHECK_UPSTREAM="$1"
+                test -n "$CHECK_UPSTREAM" || CHECK_UPSTREAM=main
+                shift || :
+                continue
+                ;;
+            -F|--fast)
+                CHECK_UPSTREAM='HEAD^'
+                shift
+                continue
+                ;;
+            -n|--dry-run)
+                TEST_ONLY=1
+                shift
+                continue
+                ;;
+            -i)
+                TEST_ONLY=0
+                shift
+                continue
+                ;;
+            --)
+                HAD_DASHDASH=1
+                shift
+                continue
+                ;;
+        esac
+    fi
+    if [ -d "$1" ]; then
+        while IFS='' read -r line;
+            do FILES+=("$line")
+        done < <(CHECK_UPSTREAM="$CHECK_UPSTREAM" g_ls_files "${1}/*.[hc]" "${EXCLUDE_PATHS[@]}")
+    elif [ -f "$1" ]; then
+        FILES+=("$1")
+    else
+        usage >&2
+        echo >&2
+        die "Unknown argument \"$1\" which also is neither a file nor a directory."
+    fi
+    shift
+    HAS_EXPLICIT_FILES=1
+done
+
+if [ $HAS_EXPLICIT_FILES = 0 ]; then
+    while IFS='' read -r line; do
+        FILES+=("$line")
+    done < <(CHECK_UPSTREAM="$CHECK_UPSTREAM" g_ls_files '*.[ch]' "${EXCLUDE_PATHS[@]}")
+fi
+
+if [ $SHOW_FILENAMES = 1 ]; then
+    for f in "${FILES[@]}" ; do
+        printf '%s\n' "$f"
+    done
+    exit 0
+fi
+
+if [ "${#FILES[@]}" = 0 ]; then
+    if [ -z "$CHECK_UPSTREAM" ]; then
+        die "Error: no files to check"
+    fi
+    exit 0
+fi
+
+FLAGS_TEST=( --Werror -n --ferror-limit=1 )
+
+if [ $TEST_ONLY = 1 ]; then
+    # We assume that all formatting is correct. In that mode, passing
+    # all filenames to clang-format is significantly faster.
+    #
+    # Only in case of an error, we iterate over the files one by one
+    # until we find the first invalid file.
+    for f in "${FILES[@]}"; do
+        [ -f "$f" ] || die "Error: file \"$f\" does not exist (or is not a regular file)"
+    done
+    clang-format "${FLAGS_TEST[@]}" "${FILES[@]}" &>/dev/null && exit 0
+    for f in "${FILES[@]}"; do
+        [ -f "$f" ] || die "Error: file \"$f\" does not exist (or is not a regular file)"
+        if ! clang-format "${FLAGS_TEST[@]}" "$f" &>/dev/null; then
+            FF="$(mktemp)"
+            trap 'rm -f "$FF"' EXIT
+            clang-format "$f" 2>/dev/null > "$FF"
+            git --no-pager diff "$f" "$FF" || :
+            die "Error: file \"$f\" has style issues."$'\n'"Fix it by running \`\"$0\" -i \"$f\"\` using $(clang-format --version)"
+        fi
+    done
+    die "an unknown error happened."
+fi
+
+clang-format -i "${FILES[@]}"
diff --git a/contrib/scripts/nm-copr-build-nm-git-bundle.sh b/contrib/scripts/nm-copr-build-nm-git-bundle.sh
new file mode 100755
index 00000000..5447a56e
--- /dev/null
+++ b/contrib/scripts/nm-copr-build-nm-git-bundle.sh
@@ -0,0 +1,95 @@
+#!/bin/bash
+
+# create a nm-git-bundle.git bundle and a SRPM for building it
+# as a package. This bundle contains the current git history
+# of upstream NetworkManager.
+#
+# The sole purpose of this is to fetch from the bundle to save
+# downloading the entire upstream git repository of NetworkManager.
+#
+# This script is also used by [1] to generate the SRPM.
+# [1] https://copr.fedorainfracloud.org/coprs/networkmanager/NetworkManager-main/package/nm-git-bundle/
+#
+# The purpose is the following. We build (many) NetworkManager packages in
+# copr. The build process runs a script (contrib/scripts/nm-copr-build.sh) that
+# fetches the git repository (and we cannot just do a shallow copy -- because
+# the version number is calculated by counts all the commits in the HEAD's
+# history).  NetworkManager's git repository is relatively large so fetching it
+# over and over is wasteful. The idea is to have a recent git-bundle of the
+# repository, which is hosted close-by in the copr infrastructure. So the build
+# script first tries to download the bundle to get the bulk of the git history,
+# before doing additional fetches from the upstream repository.  From time to
+# time, a new bundle has to be generated in copr.
+
+set -ex
+
+if [ -z "$GIT_URL" ]; then
+    GIT_URL=https://github.com/NetworkManager/NetworkManager
+    #GIT_URL=https://gitlab.freedesktop.org/NetworkManager/NetworkManager.git
+fi
+
+git clone -n "$GIT_URL"
+
+pushd NetworkManager
+
+REFS=(
+    $(git branch -a | sed -n 's#^ *remotes/origin/\(main\|nm-1-[0-9]\+\)$#\1#p')
+)
+
+unset R
+unset H
+for R in "${REFS[@]}"; do
+    H="$(git show-ref --verify --hash "refs/remotes/origin/$R")"
+    git update-ref "refs/heads/$R" "$H"
+done
+
+git bundle create nm-git-bundle.git "${REFS[@]}"
+
+popd
+
+DIR="$(mktemp -d rpmbuild.XXXXXX)"
+
+mkdir -p "$DIR/SOURCES"
+mkdir -p "$DIR/SPECS"
+
+cat <<EOF > "$DIR/SPECS/nm-git-bundle.spec"
+Name: nm-git-bundle
+Version: $(date '+%Y%m%d')
+Release: $(date '+%H%M%S')
+Summary: git-bundle of NetworkManager upstream repository
+
+License: Public Domain
+URL: https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/tree/main/contrib/fedora/rpm/nm-git-bundle.spec
+
+%global GIT_URL 'https://github.com/NetworkManager/NetworkManager'
+#global GIT_URL 'https://gitlab.freedesktop.org/NetworkManager/NetworkManager.git'
+
+Source0: nm-git-bundle.git
+
+
+BuildArch: noarch
+
+
+%description
+A git-bundle of NetworkManager upstream git repository. Useful to safe
+fetching the entire repository from the internet.
+
+
+%install
+mkdir -p %{buildroot}/usr/share/NetworkManager/
+cp %{SOURCE0} %{buildroot}/usr/share/NetworkManager/
+
+
+%files
+/usr/share/NetworkManager/nm-git-bundle.git
+EOF
+
+mv ./NetworkManager/nm-git-bundle.git "$DIR/SOURCES/"
+
+rpmbuild --define "_topdir $DIR"  -bs "$DIR/SPECS/nm-git-bundle.spec"
+
+mv "$DIR/SRPMS/"nm-git-bundle-*.src.rpm .
+mv "$DIR/SPECS/nm-git-bundle.spec" .
+mv "$DIR/SOURCES/nm-git-bundle.git" .
+rm -rf "$DIR"
+
diff --git a/contrib/scripts/nm-copr-build.sh b/contrib/scripts/nm-copr-build.sh
new file mode 100755
index 00000000..94c804fb
--- /dev/null
+++ b/contrib/scripts/nm-copr-build.sh
@@ -0,0 +1,101 @@
+#!/bin/bash
+
+# This is the build script used by our copr repository at
+#   https://copr.fedorainfracloud.org/coprs/networkmanager
+#
+# On a new upstream release, add new copr jobs named "NetworkManager-X.Y" and
+# "NetworkManager-X.Y-debug".
+#
+#   - best, look at the latest copr project and replicate the settings.
+#   - add a custom build with the following script:
+#
+#        #!/bin/bash
+#        export GIT_REF=nm-$X-$Y
+#        export DEBUG=0/1
+#        export LTO=
+#        curl https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/raw/main/contrib/scripts/nm-copr-build.sh | bash
+#
+#   - for certain CentOS/EPEL you need to add https://copr.fedorainfracloud.org/coprs/nmstate/nm-build-deps/
+#     as build chroot. See under "Settings/Project Details" for the latest copr project.
+#   - go to "Settings/Integrations" and find the notification URL for the project. Then
+#     go to https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/hooks and add
+#     a push event for the "nm-$X-$Y" branch.
+#
+# environment variables for this script:
+# - GIT_REF: the ref that should be build. Can be "main" or a git sha.
+# - DEBUG: set to 1 to build "--with debug". Otherwise the default is a release
+#     build.
+# - LTO: set to 1/0 to build "--with/--without lto", otherwise the default depends
+#     on the distribution.
+# - NM_GIT_BUNDLE: set to a HTTP url where to fetch the nm-git-bundle-*.noarch.rpm
+#     from. Set to empty to skip it. By default, it fetches the bundle from copr.
+#     See "contrib/scripts/nm-copr-build-nm-git-bundle.sh" script and
+#     https://copr.fedorainfracloud.org/coprs/networkmanager/NetworkManager-main/package/nm-git-bundle/
+
+set -ex
+
+if [[ "$DEBUG" == 1 ]]; then
+    DEBUG="--with debug"
+else
+    DEBUG="--without debug"
+fi
+
+if [ "$LTO" = 0 ]; then
+    LTO='--without lto'
+elif [ "$LTO" = 1 ]; then
+    LTO='--with lto'
+else
+    LTO=
+fi
+
+if [[ -z "$GIT_REF" ]]; then
+    echo "\$GIT_REF is not set!"
+    exit 1
+fi
+
+mkdir NetworkManager
+pushd NetworkManager
+git init .
+
+git remote add origin https://gitlab.freedesktop.org/NetworkManager/NetworkManager.git
+git remote add --no-tags github https://github.com/NetworkManager/NetworkManager
+
+get_nm_git_bundle() {
+    # try to fetch the refs from nm-git-bundle.
+    #
+    # This script runs in copr infrastructure to create the SRPM.
+    # The idea is that this URL is close and downloading it is cheaper
+    # than fetching everything from upstream git.
+    if [ -z "$NM_GIT_BUNDLE" ]; then
+        if [ -n "${NM_GIT_BUNDLE+x}" ]; then
+            return 0
+        fi
+        NM_GIT_BUNDLE='https://download.copr.fedorainfracloud.org/results/networkmanager/NetworkManager-main/fedora-38-x86_64/06008259-nm-git-bundle/nm-git-bundle-20230606-102458.noarch.rpm'
+    fi
+    mkdir nm-git-bundle
+    pushd nm-git-bundle
+    time curl "$NM_GIT_BUNDLE" \
+      | rpm2cpio - \
+      | cpio -idmv
+    popd
+    git remote add nm-git-bundle "$PWD/nm-git-bundle/usr/share/NetworkManager/nm-git-bundle.git"
+    git fetch nm-git-bundle
+}
+
+get_nm_git_bundle
+git fetch github
+git fetch origin
+git remote remove nm-git-bundle || true
+
+GIT_SHA="$(git show-ref --verify --hash "$GIT_REF" 2>/dev/null ||
+           git show-ref --verify --hash "refs/remotes/origin/$GIT_REF" 2>/dev/null ||
+           git rev-parse --verify "refs/remotes/origin/$GIT_REF" 2>/dev/null ||
+           git rev-parse --verify "$GIT_REF^{commit}" 2>/dev/null)"
+
+git checkout -b tmp "$GIT_SHA"
+
+./contrib/fedora/rpm/build_clean.sh -g -S -w test $DEBUG $LTO -s copr
+popd
+
+mv ./NetworkManager/contrib/fedora/rpm/latest/{SOURCES,SPECS}/* .
+rm -rf ./NetworkManager
diff --git a/contrib/scripts/nm-import-openconnect b/contrib/scripts/nm-import-openconnect
new file mode 100755
index 00000000..f14895d3
--- /dev/null
+++ b/contrib/scripts/nm-import-openconnect
@@ -0,0 +1,261 @@
+#!/usr/bin/env lua
+-- SPDX-License-Identifier: GPL-2.0-or-later
+--
+-- Copyright (C) 2015 Red Hat, Inc.
+--
+
+-- Script for importing/converting OpenConnect VPN configuration files for NetworkManager
+-- In general, the implementation follows the logic of import() from
+-- https://git.gnome.org/browse/network-manager-openconnect/tree/properties/nm-openconnect.c
+
+----------------------
+-- Helper functions --
+----------------------
+function read_all(in_file)
+  local f, msg = io.open(in_file, "r")
+  if not f then return nil, msg; end
+  local content = f:read("*all")
+  f:close()
+  return content
+end
+
+function uuid()
+  math.randomseed(os.time())
+  local template ='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
+  local uuid = string.gsub(template, '[xy]', function (c)
+    local v = (c == 'x') and math.random(0, 0xf) or math.random(8, 0xb)
+    return string.format('%x', v)
+  end)
+  return uuid
+end
+
+function vpn_settings_to_text(vpn_settings)
+  local t = {}
+  for k,v in pairs(vpn_settings) do
+    t[#t+1] = k.."="..v
+  end
+  return table.concat(t, "\n")
+end
+
+function usage()
+  local basename = string.match(arg[0], '[^/\\]+$') or arg[0]
+  print(basename .. " - convert/import OpenConnect VPN configuration to NetworkManager")
+  print("Usage:")
+  print("  " .. basename .. " <input-file> <output-file>")
+  print("    - converts OpenConnect VPN config to NetworkManager keyfile")
+  print("")
+  print("  " .. basename .. " --import <input-file1> <input-file2> ...")
+  print("    - imports OpenConnect VPN config(s) to NetworkManager")
+  os.exit(1)
+end
+
+
+-------------------------------------------
+-- Functions for VPN options translation --
+-------------------------------------------
+function handle_yes(t, option, value)
+  t[option] = "yes"
+end
+function handle_generic(t, option, value)
+  if not value[2] then io.stderr:write(string.format("Warning: ignoring invalid option '%s'\n", value[1])) end
+  t[option] = value[2]
+end
+
+-- global variables
+g_con_data = {}
+g_vpn_data = {}
+
+vpn2nm = {
+  ["Description"]     = { nm_opt="id",                  func=handle_generic, tbl=g_con_data },
+  ["Host"]            = { nm_opt="gateway",             func=handle_generic, tbl=g_vpn_data },
+  ["CACert"]          = { nm_opt="cacert",              func=handle_generic, tbl=g_vpn_data },
+  ["Proxy"]           = { nm_opt="proxy",               func=handle_generic, tbl=g_vpn_data },
+  ["CSDEnable"]       = { nm_opt="enable_csd_trojan",   func=handle_yes,     tbl=g_vpn_data },
+  ["CSDWrapper"]      = { nm_opt="csd_wrapper",         func=handle_generic, tbl=g_vpn_data },
+  ["UserCertificate"] = { nm_opt="usercert",            func=handle_generic, tbl=g_vpn_data },
+  ["PrivateKey"]      = { nm_opt="userkey",             func=handle_generic, tbl=g_vpn_data },
+  ["FSID"]            = { nm_opt="pem_passphrase_fsid", func=handle_yes,     tbl=g_vpn_data },
+  ["StokenSource"]    = { nm_opt="stoken_source",       func=handle_generic, tbl=g_vpn_data },
+  ["StokenString"]    = { nm_opt="stoken_string",       func=handle_generic, tbl=g_vpn_data },
+}
+
+------------------------------------------------------
+-- Read and convert the config into the global vars --
+------------------------------------------------------
+function read_and_convert(in_file)
+  local function line_split(str)
+    -- split at '=' character
+    local sep, fields = "=", {}
+    local pattern = string.format("([^%s]+)%s(.+)", sep, sep)
+    fields[1], fields[2] = str:match(pattern)
+    return fields
+  end
+
+  in_text, msg = read_all(in_file)
+  if not in_text then return false, msg end
+
+  -- loop through the config and convert it
+  for line in in_text:gmatch("[^\r\n]+") do
+    repeat
+      -- skip comments and empty lines
+      if line:find("^%s*[#;]") or line:find("^%s*$") then break end
+      -- trim leading and trailing spaces
+      line = line:find("^%s*$") and "" or line:match("^%s*(.*%S)")
+
+      local words = line_split(line)
+      local val = vpn2nm[words[1]]
+      if val then
+        if type(val) == "table" then val.func(val.tbl, val.nm_opt, words)
+        else print(string.format("debug: '%s' : val=%s"..val)) end
+      end
+    until true
+  end
+
+  -- check mandatory parameters
+  if not g_vpn_data["gateway"] then
+    local msg = in_file .. ": Not a valid OpenConnect VPN configuration"
+    return false, msg
+  end
+  return true
+end
+
+--------------------------------------------------------
+-- Create and write connection file in keyfile format --
+--------------------------------------------------------
+function write_vpn_to_keyfile(in_file, out_file)
+  connection = [[
+[connection]
+id=__NAME_PLACEHOLDER__
+uuid=__UUID_PLACEHOLDER__
+type=vpn
+autoconnect=no
+
+[ipv4]
+method=auto
+never-default=true
+
+[ipv6]
+method=auto
+
+[vpn]
+service-type=org.freedesktop.NetworkManager.openconnect
+]]
+
+  connection = connection .. vpn_settings_to_text(g_vpn_data)
+
+  local con_name = g_con_data["id"] or (out_file:gsub(".*/", ""))
+  connection = string.gsub(connection, "__NAME_PLACEHOLDER__", con_name)
+  connection = string.gsub(connection, "__UUID_PLACEHOLDER__", uuid())
+
+  -- write output file
+  local f, err = io.open(out_file, "w")
+  if not f then io.stderr:write(err) return false end
+  f:write(connection)
+  f:close()
+
+  local ofname = out_file:gsub(".*/", "")
+  io.stderr:write("Successfully converted VPN configuration: " .. in_file .. " => " .. out_file .. "\n")
+  io.stderr:write("To use the connection, do:\n")
+  io.stderr:write("# cp " .. out_file .. " /etc/NetworkManager/system-connections\n")
+  io.stderr:write("# chmod 600 /etc/NetworkManager/system-connections/" .. ofname .. "\n")
+  io.stderr:write("# nmcli con load /etc/NetworkManager/system-connections/" .. ofname .. "\n")
+  return true
+end
+
+---------------------------------------------
+-- Import VPN connection to NetworkManager --
+---------------------------------------------
+function import_vpn_to_NM(filename)
+  local lgi = require 'lgi'
+  local GLib = lgi.GLib
+  local NM = lgi.NM
+
+  -- function creating NMConnection
+  local function create_profile(name)
+    local profile = NM.SimpleConnection.new()
+
+    s_con = NM.SettingConnection.new()
+    s_vpn = NM.SettingVpn.new()
+    s_con[NM.SETTING_CONNECTION_ID] = name
+    s_con[NM.SETTING_CONNECTION_UUID] = uuid()
+    s_con[NM.SETTING_CONNECTION_TYPE] = "vpn"
+    s_vpn[NM.SETTING_VPN_SERVICE_TYPE] = "org.freedesktop.NetworkManager.openconnect"
+    for k,v in pairs(g_vpn_data) do
+      s_vpn:add_data_item(k, v)
+    end
+
+    profile:add_setting(s_con)
+    profile:add_setting(s_vpn)
+    return profile
+  end
+
+  -- callback function for add_connection()
+  local function added_cb(client, result, data)
+    local con,err,code = client:add_connection_finish(result)
+    if con then
+      print(string.format("%s: Imported to NetworkManager: %s - %s",
+                          filename, con:get_uuid(), con:get_id()))
+    else
+      io.stderr:write(code .. ": " .. err .. "\n");
+      return false
+    end
+    main_loop:quit()
+  end
+
+  local profile_name = g_con_data["id"] or string.match(filename, '[^/\\]+$') or filename
+  main_loop = GLib.MainLoop(nil, false)
+  local con = create_profile(profile_name)
+  local client = NM.Client.new()
+
+  -- send the connection to NetworkManager
+  client:add_connection_async(con, true, nil, added_cb, nil)
+
+  -- run main loop so that the callback could be called
+  main_loop:run()
+  return true
+end
+
+
+---------------------------
+-- Main code starts here --
+---------------------------
+local import_mode = false
+local infile, outfile
+
+-- parse command-line arguments
+if not arg[1] or arg[1] == "--help" or arg[1] == "-h" then usage() end
+if arg[1] == "--import" or arg[1] == "-i" then
+  infile = arg[2]
+  if not infile then usage() end
+  import_mode = true
+else
+  infile  = arg[1]
+  outfile = arg[2]
+  if not infile or not outfile then usage() end
+  if arg[3] then usage() end
+end
+
+if import_mode then
+  -- check if lgi is available
+  local success,msg = pcall(require, 'lgi')
+  if not success then
+    io.stderr:write("Lua lgi module is not available, please install it (usually lua-lgi package)\n")
+    -- print(msg)
+    os.exit(1)
+  end
+  -- read configs, convert them and import to NM
+  for i = 2, #arg do
+    ok, err_msg = read_and_convert(arg[i])
+    if ok then import_vpn_to_NM(arg[i])
+    else io.stderr:write(err_msg .. "\n") end
+    -- reset global vars
+    g_con_data = {}
+    g_vpn_data = {}
+  end
+else
+  -- read configs, convert them and write as NM keyfile connection
+  ok, err_msg = read_and_convert(infile)
+  if ok then write_vpn_to_keyfile(infile, outfile)
+  else io.stderr:write(err_msg .. "\n") end
+end
+
diff --git a/contrib/scripts/nm-import-openvpn b/contrib/scripts/nm-import-openvpn
new file mode 100755
index 00000000..6c9f39c4
--- /dev/null
+++ b/contrib/scripts/nm-import-openvpn
@@ -0,0 +1,543 @@
+#!/usr/bin/env lua
+-- SPDX-License-Identifier: GPL-2.0-or-later
+--
+-- Copyright (C) 2015 Red Hat, Inc.
+--
+
+-- Script for importing/converting OpenVPN configuration files for NetworkManager
+-- In general, the implementation follows the logic of import() from
+-- https://git.gnome.org/browse/network-manager-openvpn/tree/properties/import-export.c
+
+
+----------------------
+-- Helper functions --
+----------------------
+function read_all(in_file)
+  local f, msg = io.open(in_file, "r")
+  if not f then return nil, msg; end
+  local content = f:read("*all")
+  f:close()
+  return content
+end
+
+function uuid()
+  math.randomseed(os.time())
+  local template ='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
+  local uuid = string.gsub(template, '[xy]', function (c)
+    local v = (c == 'x') and math.random(0, 0xf) or math.random(8, 0xb)
+    return string.format('%x', v)
+  end)
+  return uuid
+end
+
+function unquote(str)
+  return (string.gsub(str, "^([\"\'])(.*)%1$", "%2"))
+end
+
+function parse_ipv4_to_bytes(ip_addr)
+  local b1,b2,b3,b4 = ip_addr:match("^(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)$")
+  b1 = tonumber(b1)
+  b2 = tonumber(b2)
+  b3 = tonumber(b3)
+  b4 = tonumber(b4)
+  return b1, b2, b3, b4
+end
+
+function is_ipv4(ip_addr)
+  local b1,b2,b3,b4 = parse_ipv4_to_bytes(ip_addr)
+  if not b1 or (b1 > 255) then return false end
+  if not b2 or (b2 > 255) then return false end
+  if not b3 or (b3 > 255) then return false end
+  if not b4 or (b4 > 255) then return false end
+  return true
+end
+
+function ip_mask_to_prefix(mask)
+  local b, prefix
+  local b1,b2,b3,b4 = parse_ipv4_to_bytes(mask)
+
+  if b4 ~= 0 then
+    prefix = 24
+    b = b4
+  elseif b3 ~= 0 then
+    prefix = 16
+    b = b3
+  elseif b2 ~= 0 then
+    prefix = 8
+    b = b2
+  else
+    prefix = 0
+    b = b1
+  end
+  while b ~= 0 do
+    prefix = prefix + 1
+    b = bit32.band(0x000000FF, bit32.lshift(b, 1))
+  end
+  return prefix
+end
+
+function vpn_settings_to_text(vpn_settings)
+  local t = {}
+  for k,v in pairs(vpn_settings) do
+    t[#t+1] = k.."="..v
+  end
+  return table.concat(t, "\n")
+end
+
+function usage()
+  local basename = string.match(arg[0], '[^/\\]+$') or arg[0]
+  print(basename .. " - convert/import OpenVPN configuration to NetworkManager")
+  print("Usage:")
+  print("  " .. basename .. " <input-file> <output-file>")
+  print("    - converts OpenVPN config to NetworkManager keyfile")
+  print("")
+  print("  " .. basename .. " --import <input-file1> <input-file2> ...")
+  print("    - imports OpenVPN config(s) to NetworkManager")
+  os.exit(1)
+end
+
+
+-------------------------------------------
+-- Functions for VPN options translation --
+-------------------------------------------
+function set_bool(t, option, value)
+  g_switches[option] = true
+end
+function handle_yes(t, option, value)
+  t[option] = "yes"
+end
+function handle_generic(t, option, value)
+  if not value[2] then io.stderr:write(string.format("Warning: ignoring invalid option '%s'\n", value[1])) return end
+  t[option] = value[2]
+end
+function handle_generic_unquote(t, option, value)
+  if not value[2] then io.stderr:write(string.format("Warning: ignoring invalid option '%s'\n", value[1])) return end
+  t[option] = unquote(value[2])
+end
+function handle_number(t, option, value)
+  if not value[2] then io.stderr:write(string.format("Warning: ignoring invalid option '%s'\n", value[1])) return end
+  if not tonumber(value[2]) then
+    io.stderr:write(string.format("Warning: ignoring not numeric value '%s' for option '%s'\n", value[2], value[1]))
+    return
+  end
+  t[option] = value[2]
+end
+function handle_proto(t, option, value)
+  if not value[2] then io.stderr:write("Warning: ignoring invalid option 'proto'\n") end
+  if value[2] == "tcp" or value[3] == "tcp-client" or value[2] == "tcp-server" then
+    t[option] = "yes"
+  end
+end
+function handle_comp_lzo(t, option, value)
+  value[2] = value[2] or "adaptive"
+  if value[2] == "no" then
+    value[2] = "no-by-default"
+  elseif value[2] ~= "yes" and value[2] ~= "adaptive" then
+    io.stderr:write(string.format("Warning: ignoring invalid argument '%s' in option 'comp-lzo'\n", value[2]))
+    return
+  end
+  t[option] = value[2]
+end
+function handle_dev_type(t, option, value)
+  if value[2] ~= "tun" and value[2] ~= "tap" then
+    io.stderr:write(string.format("Warning: ignoring invalid option '%s'\n", value[1]))
+  end
+  t[option] = value[2]
+end
+function handle_remote(t, option, value)
+  local rem
+  if not value[2] then io.stderr:write("Warning: ignoring invalid option 'remote'\n") return end
+  rem = value[2]
+  if tonumber(value[3]) then
+    rem = rem .. ":" .. value[3]
+  end
+  if value[4] == "udp" or value[4] == "tcp" then
+    rem = rem .. ":" .. value[4]
+  end
+  if t[option] then
+    t[option] = t[option] .. " " .. rem
+  else
+    t[option] = rem
+  end
+  g_switches[value[1]] = true
+end
+function handle_port(t, option, value)
+  if tonumber(value[2]) then
+    t[option] = value[2]
+  end
+end
+function handle_proxy(t, option, value)
+  if not value[2] then io.stderr:write(string.format("Warning: ignoring invalid option '%s'\n", value[1])) return end
+  if value[4] then io.stderr:write(string.format("Warning: the third argument of '%s' is not supported yet\n", value[1])) end
+  t[option[1]] = string.gsub(value[1], "-proxy", "")
+  t[option[2]] = value[2]
+  t[option[3]] = value[3]
+end
+function handle_ifconfig(t, option, value)
+  if not (value[2] and value[3]) then io.stderr:write("Warning: ignoring invalid option 'ifconfig'\n") return end
+  t[option[1]] = value[2]
+  t[option[2]] = value[3]
+end
+function handle_keepalive(t, option, value)
+  if (not (value[2] and value[3])) or (not tonumber(value[2]) or not tonumber(value[3])) then
+    io.stderr:write("Warning: ignoring invalid option 'keepalive'; two numbers required\n")
+    return
+  end
+  t[option[1]] = value[2]
+  t[option[2]] = value[3]
+end
+function handle_path(t, option, value)
+  if value[1] == "pkcs12" then
+    t["ca"] = value[2]
+    t["cert"] = value[2]
+    t["key"] = value[2]
+  else
+    t[option] = value[2]
+  end
+end
+function handle_secret(t, option, value)
+  t[option[1]] = value[2]
+  t[option[2]] = value[3]
+  g_switches[value[1]]= true
+end
+function handle_remote_cert_tls(t, option, value)
+  if value[2] ~= "client" and value[2] ~= "server" then
+    io.stderr:write(string.format("Warning: ignoring invalid option '%s'\n", value[1]))
+    return
+  end
+  t[option] = value[2]
+end
+function handle_routes(t, option, value)
+  if not value[2] then io.stderr:write("Warning: invalid option 'route'\n") return end
+  netmask = (value[3] and value[3] ~= "default") and value[3] or "255.255.255.255"
+  gateway = (value[4] and value[4] ~= "default") and value[4] or "0.0.0.0"
+  metric  = (value[5] and value[5] ~= "default") and value[5] or "0"
+
+  if not is_ipv4(value[2]) then
+    if value[2] == "vpn_gateway" or value[2] == "net_gateway" or value[2] == "remote_host" then
+      io.stderr:write(string.format("Warning: sorry, the '%s' keyword is not supported by NetworkManager in option '%s'\n",
+                      value[2], value[1]))
+    else
+      io.stderr:write(string.format("Warning: '%s' is not a valid IPv4 address in option '%s'\n", value[2], value[1]))
+    end
+    return
+  end
+  if not is_ipv4(netmask) then
+    io.stderr:write(string.format("Warning: '%s' is not a valid IPv4 netmask in option '%s'\n", netmask, value[1]))
+    return
+  end
+  if not is_ipv4(gateway) then
+    if gateway == "vpn_gateway" or gateway == "net_gateway" or gateway == "remote_host" then
+      io.stderr:write(string.format("Warning: sorry, the '%s' keyword is not supported by NetworkManager in option '%s'\n",
+                      gateway, value[1]))
+    else
+      io.stderr:write(string.format("Warning: '%s' is not a valid IPv4 gateway in option '%s'\n", gateway, value[1]))
+    end
+    return
+  end
+  if not tonumber(metric) then
+    io.stderr:write(string.format("Warning: '%s' is not a valid metric in option '%s'\n", metric, value[1]))
+    return
+  end
+
+  if not t[option] then t[option] = {} end
+  t[option][#t[option]+1] = {value[2], netmask, gateway, metric}
+end
+function handle_verify_x509_name(t, option, value)
+  if not value[2] then io.stderr:write("Warning: missing argument in option 'verify-x509-name'\n") return end
+  value[2] = unquote(value[2])
+  value[3] = value[3] or "subject"
+  if value[3] ~= "subject" and value[3] ~= "name" and value[3] ~= "name-prefix" then
+    io.stderr:write(string.format("Warning: ignoring invalid value '%s' for type in option '%s'\n", value[3], value[1]))
+    return
+  end
+  t[option] = value[3] .. ":" .. value[2]
+end
+
+-- global variables
+g_vpn_data = {}
+g_ip4_data = {}
+g_switches = {}
+
+vpn2nm = {
+  ["auth"]              = { nm_opt="auth",             func=handle_generic,         tbl=g_vpn_data },
+  ["auth-user-pass"]    = { nm_opt="auth-user-pass",   func=set_bool,               tbl={} },
+  ["ca"]                = { nm_opt="ca",               func=handle_path,            tbl=g_vpn_data },
+  ["cert"]              = { nm_opt="cert",             func=handle_path,            tbl=g_vpn_data },
+  ["cipher"]            = { nm_opt="cipher",           func=handle_generic,         tbl=g_vpn_data },
+  ["client"]            = { nm_opt="client",           func=set_bool,               tbl={} },
+  ["comp-lzo"]          = { nm_opt="comp-lzo",         func=handle_comp_lzo,        tbl=g_vpn_data },
+  ["dev"]               = { nm_opt="dev",              func=handle_generic,         tbl=g_vpn_data },
+  ["dev-type"]          = { nm_opt="dev-type",         func=handle_dev_type,        tbl=g_vpn_data },
+  ["float"]             = { nm_opt="float",            func=handle_yes,             tbl=g_vpn_data },
+  ["fragment"]          = { nm_opt="fragment-size",    func=handle_generic,         tbl=g_vpn_data },
+  ["http-proxy"]        = { nm_opt={"proxy-type", "proxy-server", "proxy-port"}, func=handle_proxy, tbl=g_vpn_data },
+  ["http-proxy-retry"]  = { nm_opt="proxy-retry",      func=handle_yes,             tbl=g_vpn_data },
+  ["ifconfig"]          = { nm_opt={"local-ip", "remote-ip"}, func=handle_ifconfig, tbl=g_vpn_data },
+  ["keepalive"]         = { nm_opt={"ping", "ping-restart"}, func=handle_keepalive, tbl=g_vpn_data },
+  ["key"]               = { nm_opt="key",              func=handle_path,            tbl=g_vpn_data },
+  ["keysize"]           = { nm_opt="keysize",          func=handle_generic,         tbl=g_vpn_data },
+  ["max-routes"]        = { nm_opt="max-routes",       func=handle_number,          tbl=g_vpn_data },
+  ["mssfix"]            = { nm_opt="mssfix",           func=handle_yes,             tbl=g_vpn_data },
+  ["ns-cert-type"]      = { nm_opt="ns-cert-type",     func=handle_remote_cert_tls, tbl=g_vpn_data },
+  ["ping"]              = { nm_opt="ping",             func=handle_number,          tbl=g_vpn_data },
+  ["ping-exit"]         = { nm_opt="ping-exit",        func=handle_number,          tbl=g_vpn_data },
+  ["ping-restart"]      = { nm_opt="ping-restart",     func=handle_number,          tbl=g_vpn_data },
+  ["pkcs12"]            = { nm_opt="client",           func=handle_path,            tbl=g_vpn_data },
+  ["port"]              = { nm_opt="port",             func=handle_port,            tbl=g_vpn_data },
+  ["proto"]             = { nm_opt="proto-tcp",        func=handle_proto,           tbl=g_vpn_data },
+  ["remote"]            = { nm_opt="remote",           func=handle_remote,          tbl=g_vpn_data },
+  ["remote-cert-tls"]   = { nm_opt="remote-cert-tls",  func=handle_remote_cert_tls, tbl=g_vpn_data },
+  ["remote-random"]     = { nm_opt="remote-random",    func=handle_yes,             tbl=g_vpn_data },
+  ["reneg-sec"]         = { nm_opt="reneg-seconds",    func=handle_generic,         tbl=g_vpn_data },
+  ["route"]             = { nm_opt="routes",           func=handle_routes,          tbl=g_ip4_data },
+  ["rport"]             = { nm_opt="port",             func=handle_port,            tbl=g_vpn_data },
+  ["secret"]            = { nm_opt={"static-key", "static-key-direction"}, func=handle_secret, tbl=g_vpn_data },
+  ["socks-proxy"]       = { nm_opt={"proxy-type", "proxy-server", "proxy-port"}, func=handle_proxy, tbl=g_vpn_data },
+  ["socks-proxy-retry"] = { nm_opt="proxy-retry",      func=handle_yes,             tbl=g_vpn_data },
+  ["tls-auth"]          = { nm_opt={"ta", "ta-dir"},   func=handle_secret,          tbl=g_vpn_data },
+  ["tls-cipher"]        = { nm_opt="tls-cipher",       func=handle_generic_unquote, tbl=g_vpn_data },
+  ["tls-client"]        = { nm_opt="client",           func=set_bool,               tbl={} },
+  ["tls-remote"]        = { nm_opt="tls-remote",       func=handle_generic_unquote, tbl=g_vpn_data },
+  ["tun-ipv6"]          = { nm_opt="tun-ipv6",         func=handle_yes,             tbl=g_vpn_data },
+  ["tun-mtu"]           = { nm_opt="tunnel-mtu",       func=handle_generic,         tbl=g_vpn_data },
+  ["verify-x509-name"]  = { nm_opt="verify-x509-name", func=handle_verify_x509_name,tbl=g_vpn_data },
+}
+
+------------------------------------------------------------
+-- Read and convert the config into the global g_vpn_data --
+-----------------------------------------------------------
+function read_and_convert(in_file)
+  local function line_split(line)
+    local t={}
+    local i, idx = 1, 1
+    local delim = "\""
+    while true do
+      local a,b = line:find("%S+", idx)
+      if not a then break end
+
+      local str = line:sub(a,b)
+      local quote = nil
+      if str:sub(1,1) == delim and str:sub(#str,#str) ~= delim then
+        quote = (line.." "):find(delim.."%s", b + 1)
+      end
+
+      if quote then
+        t[i] = line:sub(a, quote)
+        idx = quote + 1
+      else
+        t[i] = str
+        idx = b + 1
+      end
+      i = i + 1
+    end
+    return t
+  end
+
+  in_text, msg = read_all(in_file)
+  if not in_text then return false, msg end
+
+  -- loop through the config and convert it
+  for line in in_text:gmatch("[^\r\n]+") do
+    repeat
+      -- skip comments and empty lines
+      if line:find("^%s*[#;]") or line:find("^%s*$") then break end
+      -- trim leading and trailing spaces
+      line = line:find("^%s*$") and "" or line:match("^%s*(.*%S)")
+
+      local words = line_split(line)
+      local val = vpn2nm[words[1]]
+      if val then
+        if type(val) == "table" then val.func(val.tbl, val.nm_opt, words)
+        else print(string.format("debug: '%s' : val=%s"..val)) end
+      end
+    until true
+  end
+
+  -- check some inter-option dependencies
+  if not g_switches["client"] and not g_switches["secret"] then
+    local msg = in_file .. ": Not a valid OpenVPN client configuration"
+    return false, msg
+  end
+  if not g_switches["remote"] then
+    local msg = in_file .. ": Not a valid OpenVPN configuration (no remote)"
+    return false, msg
+  end
+
+  -- set 'connection-type'
+  g_vpn_data["connection-type"] = "tls"
+  have_sk = g_switches["secret"] ~= nil
+  have_ca = g_vpn_data["ca"] ~= nil
+  have_certs = ve_ca and g_vpn_data["cert"] and g_vpn_data["key"]
+  if g_switches["auth-user-pass"] then
+    if have_certs then
+      g_vpn_data["connection-type"] = "password-tls"
+    elseif have_ca then
+      g_vpn_data["connection-type"] = "tls"
+    end
+  elseif have_certs then g_vpn_data["connection-type"] = "tls"
+  elseif have_sk then g_vpn_data["connection-type"] = "static-key"
+  end
+  return true
+end
+
+
+--------------------------------------------------------
+-- Create and write connection file in keyfile format --
+--------------------------------------------------------
+function write_vpn_to_keyfile(in_file, out_file)
+  connection = [[
+[connection]
+id=__NAME_PLACEHOLDER__
+uuid=__UUID_PLACEHOLDER__
+type=vpn
+autoconnect=no
+
+[ipv4]
+method=auto
+never-default=true
+__ROUTES_PLACEHOLDER__
+
+[ipv6]
+method=auto
+
+[vpn]
+service-type=org.freedesktop.NetworkManager.openvpn
+]]
+  connection = connection .. vpn_settings_to_text(g_vpn_data)
+
+  local routes = ""
+  for idx, r in ipairs(g_ip4_data["routes"] or {}) do
+    routes = routes .. string.format("routes%d=%s/%s,%s,%s\n",
+                                     idx, r[1], ip_mask_to_prefix(r[2]), r[3], r[4])
+  end
+
+  connection = string.gsub(connection, "__NAME_PLACEHOLDER__", (out_file:gsub(".*/", "")))
+  connection = string.gsub(connection, "__UUID_PLACEHOLDER__", uuid())
+  connection = string.gsub(connection, "__ROUTES_PLACEHOLDER__\n", routes)
+
+  -- write output file
+  local f, err = io.open(out_file, "w")
+  if not f then io.stderr:write(err) return false end
+  f:write(connection)
+  f:close()
+
+  local ofname = out_file:gsub(".*/", "")
+  io.stderr:write("Successfully converted VPN configuration: " .. in_file .. " => " .. out_file .. "\n")
+  io.stderr:write("To use the connection, do:\n")
+  io.stderr:write("# cp " .. out_file .. " /etc/NetworkManager/system-connections\n")
+  io.stderr:write("# chmod 600 /etc/NetworkManager/system-connections/" .. ofname .. "\n")
+  io.stderr:write("# nmcli con load /etc/NetworkManager/system-connections/" .. ofname .. "\n")
+  return true
+end
+
+---------------------------------------------
+-- Import VPN connection to NetworkManager --
+---------------------------------------------
+function import_vpn_to_NM(filename)
+  local lgi = require 'lgi'
+  local GLib = lgi.GLib
+  local NM = lgi.NM
+
+  -- function creating NMConnection
+  local function create_profile(name)
+    local profile = NM.SimpleConnection.new()
+
+    s_con = NM.SettingConnection.new()
+    s_ip4 = NM.SettingIP4Config.new()
+    s_vpn = NM.SettingVpn.new()
+    s_con[NM.SETTING_CONNECTION_ID] = name
+    s_con[NM.SETTING_CONNECTION_UUID] = uuid()
+    s_ip4[NM.SETTING_IP_CONFIG_METHOD] = NM.SETTING_IP4_CONFIG_METHOD_AUTO
+    s_con[NM.SETTING_CONNECTION_TYPE] = "vpn"
+    s_vpn[NM.SETTING_VPN_SERVICE_TYPE] = "org.freedesktop.NetworkManager.openvpn"
+
+    -- add routes
+    local AF_INET = 2
+    for _, r in ipairs(g_ip4_data["routes"] or {}) do
+      route = NM.IPRoute.new(AF_INET, r[1], ip_mask_to_prefix(r[2]), r[3], r[4])
+      s_ip4:add_route(route)
+    end
+
+    -- add vpn data
+    for k,v in pairs(g_vpn_data) do
+      s_vpn:add_data_item(k, v)
+    end
+
+    profile:add_setting(s_con)
+    profile:add_setting(s_vpn)
+    profile:add_setting(s_ip4)
+    return profile
+  end
+
+  -- callback function for add_connection()
+  local function added_cb(client, result, data)
+    local con,err,code = client:add_connection_finish(result)
+    if con then
+      print(string.format("%s: Imported to NetworkManager: %s - %s",
+                          filename, con:get_uuid(), con:get_id()))
+    else
+      io.stderr:write(code .. ": " .. err .. "\n");
+      return false
+    end
+    main_loop:quit()
+  end
+
+  local profile_name = string.match(filename, '[^/\\]+$') or filename
+  main_loop = GLib.MainLoop(nil, false)
+  local con = create_profile(profile_name)
+  local client = NM.Client.new()
+
+  -- send the connection to NetworkManager
+  client:add_connection_async(con, true, nil, added_cb, nil)
+
+  -- run main loop so that the callback could be called
+  main_loop:run()
+  return true
+end
+
+
+---------------------------
+-- Main code starts here --
+---------------------------
+local import_mode = false
+local infile, outfile
+
+-- parse command-line arguments
+if not arg[1] or arg[1] == "--help" or arg[1] == "-h" then usage() end
+if arg[1] == "--import" or arg[1] == "-i" then
+  infile = arg[2]
+  if not infile then usage() end
+  import_mode = true
+else
+  infile  = arg[1]
+  outfile = arg[2]
+  if not infile or not outfile then usage() end
+  if arg[3] then usage() end
+end
+
+if import_mode then
+  -- check if lgi is available
+  local success,msg = pcall(require, 'lgi')
+  if not success then
+    io.stderr:write("Lua lgi module is not available, please install it (usually lua-lgi package)\n")
+    -- print(msg)
+    os.exit(1)
+  end
+  -- read configs, convert them and import to NM
+  for i = 2, #arg do
+    ok, err_msg = read_and_convert(arg[i])
+    if ok then import_vpn_to_NM(arg[i])
+    else io.stderr:write(err_msg .. "\n") end
+    -- reset global vars
+    g_vpn_data = {}
+    g_ip4_data = {}
+    g_switches = {}
+  end
+else
+  -- read configs, convert them and write as NM keyfile connection
+  ok, err_msg = read_and_convert(infile)
+  if ok then write_vpn_to_keyfile(infile, outfile)
+  else io.stderr:write(err_msg .. "\n") end
+end
+
diff --git a/contrib/scripts/nm-import-vpnc b/contrib/scripts/nm-import-vpnc
new file mode 100755
index 00000000..f7d5debb
--- /dev/null
+++ b/contrib/scripts/nm-import-vpnc
@@ -0,0 +1,416 @@
+#!/usr/bin/env lua
+-- SPDX-License-Identifier: GPL-2.0-or-later
+--
+-- Copyright (C) 2015 Red Hat, Inc.
+--
+
+-- Script for importing/converting Cisco VPN configuration files (.pcf) to NetworkManager
+-- In general, the implementation follows the logic of import() from
+-- https://git.gnome.org/browse/network-manager-vpnc/tree/properties/nm-vpnc.c
+
+----------------------
+-- Helper functions --
+----------------------
+function read_all(in_file)
+  local f, msg = io.open(in_file, "r")
+  if not f then return nil, msg; end
+  local content = f:read("*all")
+  f:close()
+  return content
+end
+
+function uuid()
+  math.randomseed(os.time())
+  local template ='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
+  local uuid = string.gsub(template, '[xy]', function (c)
+    local v = (c == 'x') and math.random(0, 0xf) or math.random(8, 0xb)
+    return string.format('%x', v)
+  end)
+  return uuid
+end
+
+function vpn_settings_to_text(vpn_settings)
+  local t = {}
+  for k,v in pairs(vpn_settings) do
+    t[#t+1] = k.."="..v
+  end
+  return table.concat(t, "\n")
+end
+
+function usage()
+  local basename = string.match(arg[0], '[^/\\]+$') or arg[0]
+  print(basename .. " - convert/import Cisco VPN (.pcf) configuration to NetworkManager")
+  print("Usage:")
+  print("  " .. basename .. " <input-file> <output-file>")
+  print("    - converts Cisco VPN config to NetworkManager keyfile")
+  print("")
+  print("  " .. basename .. " --import <input-file1> <input-file2> ...")
+  print("    - imports Cisco VPN config(s) to NetworkManager")
+  os.exit(1)
+end
+
+
+-------------------------------------------
+-- Functions for VPN options translation --
+-------------------------------------------
+function set_option(t, option, value)
+  g_switches[value[1]] = value[2]
+end
+function handle_generic(t, option, value)
+  t[option] = value[2]
+end
+function handle_yes(t, option, value)
+  t[option] = "yes"
+end
+function handle_bool(t, option, value)
+  if tonumber(value[2]) == 1 then
+    t[option] = "true"
+  elseif tonumber(value[2]) == 0 then
+    t[option] = "false"
+  else
+    io.stderr:write(string.format("Warning: ignoring invalid option '%s'\n", value[1]))
+  end
+end
+function handle_DHGroup(t, option, value)
+  local dhgroups = { [1]="dh1", [2]="dh2", [5]="dh5" }
+  dhgroup = dhgroups[tonumber(value[2])]
+  if not dhgroup then io.stderr:write(string.format("Warning: invalid value for 'DHGroup': %s\n", value[2])) end
+  t[option] = dhgroup
+end
+function handle_PeerTimeout(t, option, value)
+  if not value[2] then io.stderr:write("Warning: ignoring invalid option 'PeerTimeout'\n") end
+  if tonumber(value[2]) == 0 or (tonumber(value[2]) >=10 and tonumber(value[2] <= 86400)) then
+    t[option] = value[2]
+  else io.stderr:write(string.format("Warning: invalid value for 'PeerTimeout': %s\n", value[2])) end
+end
+function handle_(t, option, value)
+  io.stderr:write("Warning: enc_GroupPwd: encrypted group passwords are not supported by this script.\n")
+end
+function handle_TunnelingMode(t, option, value)
+  if value[2] == 1 then
+    io.stderr:write("Warning: TCP tunneling is not supported by vpnc. " ..
+                    "The connection will be used with TCP tunneling disabled, " ..
+                    "however it may not work as expected.\n")
+  end
+end
+function handle_UseLegacyIKEPort(t, option, value)
+  if value[2] ~= 0 then
+    t[option] = 500
+  end
+end
+function handle_routes(t, option, value)
+  local function splitroutes(str)
+    local sep, fields = " ", {}
+    local pattern = string.format("([^%s]+)", sep)
+    str:gsub(pattern,
+             function(c)
+               local c1,c2 = c:match("^(%d+%.%d+%.%d+%.%d+)/(%d+)$")
+               if c1 then
+                 fields[#fields+1] = { c1, c2 }
+               else
+                 io.stderr:write("Warning: ignoring invalid route: '" .. c .. "'\n")
+               end
+             end)
+    return fields
+  end
+  t[option] = splitroutes(value[2])
+end
+
+-- global variables -
+g_vpn_data = {}
+g_vpn_pwds = {}
+g_con_data = {}
+g_ip4_data = {}
+g_switches = {}
+
+vpn2nm = {
+  ["Description"]            = { nm_opt="id",                func=handle_generic, tbl=g_con_data },
+  ["InterfaceName"]          = { nm_opt="interface-name",    func=handle_generic, tbl=g_con_data },
+  ["EnableLocalLAN"]         = { nm_opt="never-default",     func=handle_bool,    tbl=g_ip4_data },
+  ["X-NM-Routes"]            = { nm_opt="routes",            func=handle_routes,  tbl=g_ip4_data },
+  ["Host"]                   = { nm_opt="IPSec gateway",     func=handle_generic, tbl=g_vpn_data },
+  ["GroupName"]              = { nm_opt="IPSec ID",          func=handle_generic, tbl=g_vpn_data },
+  ["Username"]               = { nm_opt="Xauth username",    func=handle_generic, tbl=g_vpn_data },
+  ["UserPassword"]           = { nm_opt="Xauth password",    func=handle_generic, tbl=g_vpn_pwds },
+  ["SaveUserPassword"]       = { nm_opt="",                  func=set_option,     tbl={}         },
+  ["GroupPwd"]               = { nm_opt="IPSec secret",      func=handle_generic, tbl=g_vpn_pwds },
+  ["DHGroup"]                = { nm_opt="IKE DH Group",      func=handle_DHGroup, tbl=g_vpn_data },
+  ["NTDomain"]               = { nm_opt="Domain",            func=handle_generic, tbl=g_vpn_data },
+  ["SingleDES"]              = { nm_opt="Enable Single DES", func=handle_yes,     tbl=g_vpn_data },
+  ["EnableNat"]              = { nm_opt="",                  func=set_option,     tbl={}         },
+  ["X-NM-Use-NAT-T"]         = { nm_opt="",                  func=set_option,     tbl={}         },
+  ["X-NM-Force-NAT-T"]       = { nm_opt="",                  func=set_option,     tbl={}         },
+  ["X-NM-SaveGroupPassword"] = { nm_opt="",                  func=set_option,     tbl={}         },
+  ["UseLegacyIKEPort"]       = { nm_opt="Local Port",        func=handle_UseLegacyIKEPort,      tbl=g_vpn_data },
+  ["PeerTimeout"]            = { nm_opt="DPD idle timeout (our side)", func=handle_PeerTimeout, tbl=g_vpn_data },
+  ["TunnelingMode"]          = { nm_opt="",                  func=handle_TunnelingMode, tbl= {} },
+  ["enc_UserPassword"]       = { nm_opt="",                  func=handle_enc_pwd,       tbl= {} },
+  ["enc_GroupPwd"]           = { nm_opt="",                  func=handle_enc_pwd,       tbl= {} },
+}
+
+------------------------------------------------------
+-- Read and convert the config into the global vars --
+------------------------------------------------------
+function read_and_convert(in_file)
+  local function line_split(str)
+    -- split at '=' character
+    local sep, fields = "=", {}
+    local pattern = string.format("([^%s]+)%s(.+)", sep, sep)
+    fields[1], fields[2] = str:match(pattern)
+    return fields
+  end
+
+  in_text, msg = read_all(in_file)
+  if not in_text then return false, msg end
+
+  -- loop through the config and convert it
+  for line in in_text:gmatch("[^\r\n]+") do
+    repeat
+      -- skip comments and empty lines
+      if line:find("^%s*[#;]") or line:find("^%s*$") then break end
+      -- trim leading and trailing spaces
+      line = line:find("^%s*$") and "" or line:match("^%s*(.*%S)")
+
+      local words = line_split(line)
+      local val = vpn2nm[words[1]]
+      if val then
+        if type(val) == "table" then val.func(val.tbl, val.nm_opt, words)
+        else print(string.format("debug: '%s': val=%s", line, val)) end
+      end
+    until true
+  end
+
+  -- check if mandatory options exist
+  if not g_vpn_data["IPSec gateway"] then
+    local msg = in_file .. ": Not a valid Cisco VPN configuration (no Host)"
+    return false, msg
+  end
+  if not g_vpn_data["IPSec ID"] then
+    local msg = in_file .. ": Not a valid OpenVPN configuration (no GroupName)"
+    return false, msg
+  end
+
+  -- process inter-option dependencies
+  -- NAT traversal mode
+  local natt_mode = {
+    NONE = "none",
+    NATT = "natt",
+    NATT_ALWAYS = "force-natt",
+    CISCO = "cisco-udp"
+  }
+  g_vpn_data["NAT Traversal Mode"] = natt_mode.CISCO
+  if tonumber(g_switches["EnableNat"]) == 0 then
+    g_vpn_data["NAT Traversal Mode"] = natt_mode.NONE
+  elseif tonumber(g_switches["EnableNat"]) == 1 then
+    if tonumber(g_switches["X-NM-Force-NAT-T"]) == 1 then
+      g_vpn_data["NAT Traversal Mode"] = natt_mode.NATT_ALWAYS
+    elseif tonumber(g_switches["X-NM-Use-NAT-T"]) == 1 then
+      g_vpn_data["NAT Traversal Mode"] = natt_mode.NATT
+    end
+  else
+    io.stderr:write("Warning: invalid value for EnableNat\n")
+    g_vpn_data["NAT Traversal Mode"] = natt_mode.CISCO
+  end
+
+  -- set secret flags
+  g_vpn_data["Xauth password-flags"] = 1
+  if tonumber(g_switches["SaveUserPassword"]) == 1 then
+    g_vpn_data["xauth-password-type"] = "save"
+  else
+    g_vpn_data["Xauth password-flags"] = 3
+  end
+  if g_vpn_data["IPSec ID"] then
+    g_vpn_data["IPSec ID-flags"] = 1
+  end
+  if g_switches["X-NM-SaveGroupPassword"] then
+    if tonumber(g_switches["X-NM-SaveGroupPassword"]) == 1 then
+      g_vpn_data["ipsec-secret-type"] = "save"
+      g_vpn_data["IPSec ID-flags"] = 1
+    else
+      g_vpn_data["IPSec ID-flags"] = 3
+    end
+  else
+    g_vpn_data["ipsec-secret-type"] = "save"
+  end
+
+  return true
+end
+
+
+--------------------------------------------------------
+-- Create and write connection file in keyfile format --
+--------------------------------------------------------
+function write_vpn_to_keyfile(in_file, out_file)
+  connection = [[
+[connection]
+id=__NAME_PLACEHOLDER__
+uuid=__UUID_PLACEHOLDER__
+__IFNAME_PLACEHOLDER__
+type=vpn
+autoconnect=no
+
+[ipv4]
+method=auto
+never-default=__NEVER_DEFAULT_PLACEHOLDER__
+__ROUTES_PLACEHOLDER__
+
+[ipv6]
+method=auto
+
+[vpn]
+service-type=org.freedesktop.NetworkManager.vpnc
+]]
+  connection = connection .. vpn_settings_to_text(g_vpn_data)
+  connection = connection .. "\n\n[vpn-secrets]\n"
+  connection = connection .. vpn_settings_to_text(g_vpn_pwds)
+
+  local con_name = g_con_data["id"] or (out_file:gsub(".*/", ""))
+  local ifname = g_con_data["interface-name"]
+  local never_default = g_ip4_data["never-default"] or "false"
+  local routes = ""
+  if ifname then ifname = "interface-name="..ifname.."\n"  else ifname = "" end
+  for idx, r in ipairs(g_ip4_data["routes"] or {}) do
+    routes = routes .. string.format("routes%d=%s/%s\n", idx, r[1], r[2])
+  end
+
+  connection = string.gsub(connection, "__NAME_PLACEHOLDER__", con_name)
+  connection = string.gsub(connection, "__UUID_PLACEHOLDER__", uuid())
+  connection = string.gsub(connection, "__IFNAME_PLACEHOLDER__\n", ifname)
+  connection = string.gsub(connection, "__NEVER_DEFAULT_PLACEHOLDER__", never_default)
+  connection = string.gsub(connection, "__ROUTES_PLACEHOLDER__\n", routes)
+
+  -- write output file
+  local f, err = io.open(out_file, "w")
+  if not f then io.stderr:write(err) return false end
+  f:write(connection)
+  f:close()
+
+  local ofname = out_file:gsub(".*/", "")
+  io.stderr:write("Successfully converted VPN configuration: " .. in_file .. " => " .. out_file .. "\n")
+  io.stderr:write("To use the connection, do:\n")
+  io.stderr:write("# cp " .. out_file .. " /etc/NetworkManager/system-connections\n")
+  io.stderr:write("# chmod 600 /etc/NetworkManager/system-connections/" .. ofname .. "\n")
+  io.stderr:write("# nmcli con load /etc/NetworkManager/system-connections/" .. ofname .. "\n")
+  return true
+end
+
+---------------------------------------------
+-- Import VPN connection to NetworkManager --
+---------------------------------------------
+function import_vpn_to_NM(filename)
+  local lgi = require 'lgi'
+  local GLib = lgi.GLib
+  local NM = lgi.NM
+
+  -- function creating NMConnection
+  local function create_profile(name)
+    local profile = NM.SimpleConnection.new()
+    local never_default = g_ip4_data["never-default"] == "true"
+
+    s_con = NM.SettingConnection.new()
+    s_vpn = NM.SettingVpn.new()
+    s_ip4 = NM.SettingIP4Config.new()
+
+    s_con[NM.SETTING_CONNECTION_ID] = name
+    s_con[NM.SETTING_CONNECTION_UUID] = uuid()
+    s_con[NM.SETTING_CONNECTION_INTERFACE_NAME] = g_con_data["interface-name"]
+    s_con[NM.SETTING_CONNECTION_TYPE] = "vpn"
+    s_vpn[NM.SETTING_VPN_SERVICE_TYPE] = "org.freedesktop.NetworkManager.vpnc"
+    s_ip4[NM.SETTING_IP_CONFIG_METHOD] = NM.SETTING_IP4_CONFIG_METHOD_AUTO
+    s_ip4[NM.SETTING_IP_CONFIG_NEVER_DEFAULT] = never_default
+
+    -- add routes
+    local AF_INET = 2
+    for _, r in ipairs(g_ip4_data["routes"] or {}) do
+      route = NM.IPRoute.new(AF_INET, r[1], r[2], nil, -1)
+      s_ip4:add_route(route)
+    end
+
+    -- add vpn data
+    for k,v in pairs(g_vpn_data) do
+      s_vpn:add_data_item(k, v)
+    end
+    -- add vpn secrets
+    for k,v in pairs(g_vpn_pwds) do
+      s_vpn:add_secret(k, v)
+    end
+
+    profile:add_setting(s_con)
+    profile:add_setting(s_vpn)
+    profile:add_setting(s_ip4)
+    return profile
+  end
+
+  -- callback function for add_connection()
+  local function added_cb(client, result, data)
+    local con,err,code = client:add_connection_finish(result)
+    if con then
+      print(string.format("%s: Imported to NetworkManager: %s - %s",
+                          filename, con:get_uuid(), con:get_id()))
+    else
+      io.stderr:write(code .. ": " .. err .. "\n");
+      return false
+    end
+    main_loop:quit()
+  end
+
+  local profile_name = g_con_data["id"] or string.match(filename, '[^/\\]+$') or filename
+  main_loop = GLib.MainLoop(nil, false)
+  local con = create_profile(profile_name)
+  local client = NM.Client.new()
+
+  -- send the connection to NetworkManager
+  client:add_connection_async(con, true, nil, added_cb, nil)
+
+  -- run main loop so that the callback could be called
+  main_loop:run()
+  return true
+end
+
+
+---------------------------
+-- Main code starts here --
+---------------------------
+local import_mode = false
+local infile, outfile
+
+-- parse command-line arguments
+if not arg[1] or arg[1] == "--help" or arg[1] == "-h" then usage() end
+if arg[1] == "--import" or arg[1] == "-i" then
+  infile = arg[2]
+  if not infile then usage() end
+  import_mode = true
+else
+  infile  = arg[1]
+  outfile = arg[2]
+  if not infile or not outfile then usage() end
+  if arg[3] then usage() end
+end
+
+if import_mode then
+  -- check if lgi is available
+  local success,msg = pcall(require, 'lgi')
+  if not success then
+    io.stderr:write("Lua lgi module is not available, please install it (usually lua-lgi package)\n")
+    -- print(msg)
+    os.exit(1)
+  end
+  -- read configs, convert them and import to NM
+  for i = 2, #arg do
+    ok, err_msg = read_and_convert(arg[i])
+    if ok then import_vpn_to_NM(arg[i])
+    else io.stderr:write(err_msg .. "\n") end
+    -- reset global vars
+    g_vpn_data = {}
+    g_vpn_pwds = {}
+    g_con_data = {}
+    g_ip4_data = {}
+    g_switches = {}
+  end
+else
+  -- read configs, convert them and write as NM keyfile connection
+  ok, err_msg = read_and_convert(infile)
+  if ok then write_vpn_to_keyfile(infile, outfile)
+  else io.stderr:write(err_msg .. "\n") end
+end
+
diff --git a/contrib/scripts/nm-python-black-format.sh b/contrib/scripts/nm-python-black-format.sh
new file mode 100755
index 00000000..eae84fdb
--- /dev/null
+++ b/contrib/scripts/nm-python-black-format.sh
@@ -0,0 +1,100 @@
+#!/bin/bash
+
+set -e
+
+_print() {
+    printf '%s\n' "$*" >&2
+}
+
+die() {
+    _print "$*"
+    exit 1
+}
+
+NM_ROOT="$(git rev-parse --show-toplevel)" || die "not inside a git repository"
+NM_PREFIX="$(git rev-parse --show-prefix)" || die "not inside a git repository"
+
+cd "$NM_ROOT" || die "failed to cd into \$NM_ROOT\""
+
+if [ ! -f "./src/core/main.c" ]; then
+    die "Error: \"$NM_ROOT\" does not look like NetworkManager source tree"
+fi
+
+BLACK="${BLACK:-black}"
+
+if ! command -v "$BLACK" &> /dev/null; then
+    _print "Error: black is not installed. On RHEL/Fedora/CentOS run 'dnf install black'"
+    exit 77
+fi
+
+OLD_IFS="$IFS"
+
+usage() {
+    printf "Usage: %s [OPTION]...\n" "$(basename "$0")"
+    printf "Reformat python source files using python black.\n\n"
+    printf "OPTIONS:\n"
+    printf "    -i                   Reformat files (this is the default)\n"
+    printf "    -n|--dry-run|--check Only check the files (contrary to \"-i\")\n"
+    printf "    --show-filenames     Only print the filenames that would be checked/formatted\n"
+    printf "    -h                   Print this help message\n"
+}
+
+TEST_ONLY=0
+SHOW_FILENAMES=0
+
+while (( $# )); do
+    case "$1" in
+        -h)
+            usage
+            exit 0
+            ;;
+        -n|--dry-run|--check)
+            TEST_ONLY=1
+            shift
+            continue
+            ;;
+        -i)
+            TEST_ONLY=0
+            shift
+            continue
+            ;;
+        --show-filenames)
+            SHOW_FILENAMES=1
+            shift
+            continue
+            ;;
+        *)
+            usage
+            exit 1
+            ;;
+    esac
+done
+
+IFS=$'\n'
+FILES=()
+FILES+=( $(git ls-tree --name-only -r HEAD | grep '\.py$') )
+FILES+=( $(git grep -l '#!.*\<p[y]thon3\?\>') )
+FILES=( $(printf "%s\n" "${FILES[@]}" | sort -u) )
+
+# Filter out paths that are forked from upstream projects and not
+# ours to reformat.
+FILES=( $(
+    printf "%s\n" "${FILES[@]}" |
+    sed \
+        -e '/^src\/[cn]-[^/]\+\//d' \
+        -e '/^src\/libnm-systemd-[^/]\+\/src\//d'
+) )
+
+IFS="$OLD_IFS"
+
+if [ $SHOW_FILENAMES = 1 ]; then
+    printf '%s\n' "${FILES[@]}"
+    exit 0
+fi
+
+EXTRA_ARGS=()
+if [ $TEST_ONLY = 1 ]; then
+    EXTRA_ARGS+=('--check')
+fi
+
+"$BLACK" "${EXTRA_ARGS[@]}" "${FILES[@]}"
diff --git a/contrib/scripts/nm-setup-git.sh b/contrib/scripts/nm-setup-git.sh
new file mode 100755
index 00000000..32e059ad
--- /dev/null
+++ b/contrib/scripts/nm-setup-git.sh
@@ -0,0 +1,134 @@
+#!/bin/bash
+
+set -e
+
+usage() {
+    printf "%s [--no-test]\n" "$CMD_NAME"
+    printf "\n"
+    printf "This script configures (or shows configuration) to the local git, with\n"
+    printf "settings that might be useful when working on NetworkManager.\n"
+    printf "\n"
+    printf "RUn it without arguments, it only prints and shows what it would do.\n"
+    printf "\n"
+    printf "  --no-test: by default, the script only prints what it\n"
+    printf "    would do. You can also set NO_TEST=1 environment variable.\n"
+    printf "\n"
+}
+
+get_bool() {
+    local name="$1"
+    local val="${!name}"
+
+    case "$val" in
+        1|y|yes|Yes|YES|true|True|TRUE|on|On|ON)
+            echo -n 1
+            return 0
+            ;;
+        0|n|no|No|NO|false|False|FALSE|off|Off|OFF)
+            echo -n 0
+            return 0
+            ;;
+        *)
+            printf "%s" "$2"
+            ;;
+    esac
+}
+
+die() {
+    echo "ERROR: $*"
+    exit 1
+}
+
+_pprint() {
+    local a
+    local sp=''
+
+    for a; do
+        printf "$sp%q" "$a"
+        sp=' '
+    done
+}
+
+call() {
+    local m=""
+
+    [ "$SKIP" = 1 ] && m="SKIP: "
+
+    if [ "$NO_TEST" != 1 ]; then
+        printf "WOULD: %s%s\n" "$m" "$(_pprint "$@")"
+        return 0
+    fi
+    printf "CALL: %s%s\n" "$m" "$(_pprint "$@")"
+    [ "$SKIP" = 1 ] || "$@"
+}
+
+git_config_reset() {
+    local key="$1"
+    local val="$2"
+    local c=(git config --replace-all "$key" "$val")
+
+    test "$#" -eq 2 || die "invalid arguments to git_config_add(): $@"
+
+    if [ "$(git config --get-all "$key")" = "$val" ]; then
+        SKIP=1 call "${c[@]}"
+        return 0
+    fi
+    call "${c[@]}"
+}
+
+git_config_add() {
+    local key="$1"
+    local val="$2"
+    local c=(git config --add "$key" "$val")
+
+    test "$#" -eq 2 || die "invalid arguments to git_config_add(): $@"
+
+    if git config --get-all "$key" | grep -qFx "$val"; then
+        SKIP=1 call "${c[@]}"
+        return 0
+    fi
+    call "${c[@]}"
+}
+
+CMD_NAME="$0"
+NO_TEST="$(get_bool NO_TEST 0)"
+
+for a; do
+    case "$a" in
+        --no-test)
+            NO_TEST=1
+            ;;
+        -h|--help)
+            usage
+            exit 0
+            ;;
+        *)
+            usage
+            die "Invalid argument \"$a\""
+            ;;
+    esac
+done
+
+case "$(git config --get-all remote.origin.url)" in
+    "https://gitlab.freedesktop.org/NetworkManager/NetworkManager.git"| \
+    "git@gitlab.freedesktop.org:NetworkManager/NetworkManager.git"| \
+    "ssh://git@gitlab.freedesktop.org/NetworkManager/NetworkManager")
+        ;;
+    *)
+        die "unexpected git repository. Expected that remote.origin.url is set to \"https://gitlab.freedesktop.org/NetworkManager/NetworkManager.git\""
+        ;;
+esac
+
+git_config_add blame.ignoreRevsFile '.git-blame-ignore-revs'
+git_config_reset blame.markIgnoredLines true
+git_config_reset blame.markUnblamableLines true
+git_config_add notes.displayref 'refs/notes/bugs'
+git_config_add remote.origin.fetch 'refs/notes/bugs:refs/notes/bugs'
+git_config_reset remote.origin.pushurl 'git@gitlab.freedesktop.org:NetworkManager/NetworkManager.git'
+git_config_add 'alias.backport-merge' '! (git show main:contrib/scripts/git-backport-merge || git show origin/main:contrib/scripts/git-backport-merge) | bash -s -'
+
+if [ "$NO_TEST" != 1 ]; then
+    printf "Run with \"--no-test\" or see \"-h\"\n" >&2
+    printf "\n" >&2
+    printf "    \"%s\" --no-test\n" "$CMD_NAME" >&2
+fi
diff --git a/contrib/scripts/test-create-many-device-setup.sh b/contrib/scripts/test-create-many-device-setup.sh
new file mode 100755
index 00000000..55f2a1c6
--- /dev/null
+++ b/contrib/scripts/test-create-many-device-setup.sh
@@ -0,0 +1,136 @@
+#!/bin/bash
+
+set -x
+
+die() {
+    printf '%s\n' "$*" >&1
+    exit 1
+}
+
+ARG_OP="$1"
+shift
+test -n "$ARG_OP" || die "specify the operation (setup, cleanup)"
+
+test "$USER" = root || die "must run as root"
+
+NUM_DEVS="${NUM_DEVS:-50}"
+NUM_VLAN_DEVS="${NUM_VLAN_DEVS:-0}"
+
+
+DNSMASQ_PIDFILE="/tmp/nm-test-create-many-device-setup.dnsmasq.pid"
+NM_TEST_CONF="/etc/NetworkManager/conf.d/99-my-test.conf"
+TEST_NETNS="T"
+
+
+_do_service() {
+    test "$DO_SERVICE" = 1 || return 0
+    "$@"
+}
+
+_dnsmasq_kill() {
+    pkill -F "$DNSMASQ_PIDFILE"
+    rm -rf "$DNSMASQ_PIDFILE"
+}
+
+_link_delete_all() {
+    ip link | sed -n 's/^[0-9]\+:.*\(t-[^@:]\+\)@.*/\1/p' | xargs -n 1 ip link delete
+}
+
+cleanup_base() {
+    ip netns delete "$TEST_NETNS"
+    _dnsmasq_kill
+    _link_delete_all
+    rm -rf "$NM_TEST_CONF"
+    rm -rf /run/NetworkManager/system-connections/c-*.nmconnection
+}
+
+cmd_cleanup() {
+    _do_service systemctl stop NetworkManager
+    cleanup_base
+    systemctl unmask NetworkManager-dispatcher
+    systemctl enable NetworkManager-dispatcher
+    _do_service systemctl start NetworkManager
+}
+
+cmd_setup() {
+
+    _do_service systemctl stop NetworkManager
+    systemctl mask NetworkManager-dispatcher
+    systemctl stop NetworkManager-dispatcher
+
+    cleanup_base
+
+    ip netns add "$TEST_NETNS"
+    ip --netns "$TEST_NETNS" link add t-br0 type bridge
+    ip --netns "$TEST_NETNS" link set t-br0 type bridge stp_state 0
+    ip --netns "$TEST_NETNS" link set t-br0 up
+    ip --netns "$TEST_NETNS" addr add 172.16.0.1/16 dev t-br0
+    ip netns exec "$TEST_NETNS" \
+        dnsmasq \
+            --conf-file=/dev/null \
+            --pid-file="$DNSMASQ_PIDFILE" \
+            --no-hosts \
+            --keep-in-foreground \
+            --bind-interfaces \
+            --except-interface=lo \
+            --clear-on-reload \
+            --listen-address=172.16.0.1 \
+            --dhcp-range=172.16.1.1,172.16.20.1,60 \
+            --no-ping \
+            &
+    disown
+    for i in `seq "$NUM_DEVS"`; do
+        ip --netns "$TEST_NETNS" link add t-a$i type veth peer t-b$i
+        ip --netns "$TEST_NETNS" link set t-a$i up
+        ip --netns "$TEST_NETNS" link set t-b$i up master t-br0
+    done
+    for i in `seq "$NUM_VLAN_DEVS"`; do
+        ip --netns "$TEST_NETNS" link add link t-b1 name t-b1.$i type vlan id $i
+        ip --netns "$TEST_NETNS" link set t-b1.$i up master t-br0
+    done
+
+    cat <<EOF > "$NM_TEST_CONF"
+[main]
+dhcp=internal
+no-auto-default=interface-name:t-a*
+[device-99-my-test]
+match-device=interface-name:t-a*
+managed=1
+[logging]
+level=INFO
+[connectivity]
+enabled=0
+EOF
+
+    _do_service systemctl start NetworkManager
+
+    for i in `seq "$NUM_DEVS"`; do
+      ip --netns "$TEST_NETNS" link set t-a$i netns $$
+    done
+
+    if [ "$DO_ADD_CON" = 1 ]; then
+        for i in `seq "$NUM_DEVS"`; do
+            nmcli connection add save no type ethernet con-name c-a$i ifname t-a$i autoconnect no ipv4.method auto ipv6.method auto
+        done
+    fi
+
+    if [ "$DO_ADD_VLAN_CON" = 1 ]; then
+        for i in `seq "$NUM_VLAN_DEVS"`; do
+            nmcli connection add save no type bridge con-name c-a1.$i-br ifname t-a1.$i.br autoconnect no ipv4.method auto ipv6.method auto bridge.stp 0
+            nmcli connection add save no type vlan   con-name c-a1.$i-po ifname t-a1.$i.po autoconnect no vlan.id $i vlan.parent t-a1 master c-a1.$i-br slave-type bridge
+        done
+    fi
+}
+
+
+case "$ARG_OP" in
+    "setup")
+        cmd_setup
+        ;;
+    "cleanup")
+        cmd_cleanup
+        ;;
+    *)
+        die "Unknown command \"$ARG_OP\""
+        ;;
+esac
diff --git a/contrib/scripts/test-macsec b/contrib/scripts/test-macsec
new file mode 100755
index 00000000..93935865
--- /dev/null
+++ b/contrib/scripts/test-macsec
@@ -0,0 +1,102 @@
+#!/bin/sh
+
+# Test for MACsec in PSK mode
+
+if [ "$#" = 2 ]; then
+    # DHCP helper
+    dev=$1
+    addr=$2
+    net=${addr%.*}
+
+    while [ ! -d "/sys/class/net/$dev" ]; do
+	    sleep 1
+    done
+
+    ip a add $addr/24 dev "$dev"
+
+    dnsmasq --conf-file --no-hosts --keep-in-foreground --listen-address=$addr \
+            --dhcp-range=$net.250,$net.255,60m  -i "$dev" \
+            --bind-interface --except-interface=lo
+
+    exit 0
+fi
+
+TMPDIR=$(mktemp -d /tmp/macsec-XXXXXX)
+ADDR=172.16.10.1
+MKA_CAK=00112233445566778899001122334455
+MKA_CKN=5544332211009988776655443322110055443322110099887766554433221100
+
+trap 'rm -rf "$TMPDIR"; kill $(jobs -p)' EXIT
+
+echo "* Setup..."
+
+# Clean up
+ip netns del macsec-ns 2> /dev/null
+ip link del macsec-veth 2> /dev/null
+# Create namespace
+ip netns add macsec-ns
+# Create interfaces
+ip link add macsec-veth type veth peer name macsec-vethp
+# Move interfaces into namespace
+ip link set macsec-vethp netns macsec-ns
+# Bring up interfaces
+ip link set macsec-veth up
+ip -n macsec-ns link set macsec-vethp up
+
+echo "* Start wpa_supplicant..."
+
+cat <<EOF > $TMPDIR/wpa_supplicant.conf
+ctrl_interface=/run/hostapd1
+eapol_version=3
+ap_scan=0
+fast_reauth=1
+network={
+	key_mgmt=NONE
+	eapol_flags=0
+	macsec_policy=1
+	mka_cak=$MKA_CAK
+	mka_ckn=$MKA_CKN
+}
+EOF
+ip netns exec macsec-ns wpa_supplicant \
+   -c "$TMPDIR/wpa_supplicant.conf" -i macsec-vethp -Dmacsec_linux -dd > /dev/null 2>&1 &
+ip netns exec macsec-ns $0 macsec0 $ADDR > /dev/null 2>&1 &
+
+echo "* Create connections..."
+
+nmcli connection delete test-macsec+ test-veth+ > /dev/null 2>&1
+nmcli connection add type ethernet ifname macsec-veth con-name test-veth+ \
+      ipv4.method disabled ipv6.method ignore
+nmcli connection add type macsec con-name test-macsec+ ifname macsec0 \
+      connection.autoconnect no \
+      macsec.parent macsec-veth macsec.mode psk \
+      macsec.mka-cak $MKA_CAK \
+      macsec.mka-cak-flags 0 \
+      macsec.mka-ckn $MKA_CKN
+
+echo "* Bring up connections..."
+nmcli connection up test-veth+
+nmcli connection up test-macsec+
+
+echo "* Test connectivity..."
+ping $ADDR -c2 -q > /dev/null
+res=$?
+
+echo "* Clean up..."
+
+nmcli connection delete test-macsec+ test-veth+ > /dev/null 2>&1
+ip link del macsec-veth 2> /dev/null
+ip netns del macsec-ns 2> /dev/null
+
+echo
+
+if [ "$res" = 0 ]; then
+	echo "Success"
+else
+	echo "Failure"
+fi
+
+exit $res
+
+
+
diff --git a/contrib/scripts/test-ppp.sh b/contrib/scripts/test-ppp.sh
new file mode 100755
index 00000000..c100e976
--- /dev/null
+++ b/contrib/scripts/test-ppp.sh
@@ -0,0 +1,108 @@
+#!/bin/bash
+
+# test-ppp.sh:
+#
+# Test script that creates an netns and connect it with
+# veth pairs. On the other end, it runs pppoe-server.
+# It also creates a NetworkManager profile that can be activated.
+#
+# Usage:
+#
+# ./test-ppp.sh [setup]: create the setup. This implies a "cleanup"
+#   first.
+# ./test-ppp.sh cleanup: cleanup the things that the script created.
+set -e
+
+export IFACE=net1
+export IFACE_PEER=net1-x
+export CON_NAME="ppp-$IFACE"
+export NETNS=nm-ppp
+export PPP_SERVICE=isp
+export PPP_AUTH=pap
+export PPP_USER=test-user
+export PPP_PASSWD=test-passwd
+export IP_PEER="192.168.133.6"
+export IP_RANGE="192.168.133.100-130"
+
+die() {
+    printf '%s\n' "$*" >&2
+    exit 1
+}
+
+do_cleanup() {
+    pkill -F "/tmp/nm-test-ppp-$IFACE.pid" pppoe-server &>/dev/null || :
+    rm -rf \
+        "/tmp/nm-test-ppp-$IFACE.pid" \
+        "/tmp/nm-test-ppp-allip-$IFACE" \
+        "/tmp/nm-test-ppp-pppoe-server-options-$IFACE" \
+        "/tmp/nm-test-ppp-$IFACE-$PPP_AUTH-secrets"
+    ip --netns "$NETNS" link delete "$IFACE_PEER" &>/dev/null || :
+    ip netns delete "$NETNS" &>/dev/null || :
+
+    nmcli connection delete id ppp-net1 || :
+}
+
+do_setup() {
+    do_cleanup
+
+    ip netns add "$NETNS"
+    ip --netns "$NETNS" link add "$IFACE" type veth peer "$IFACE_PEER"
+    ip --netns "$NETNS" link set "$IFACE_PEER" up
+
+    ip --netns "$NETNS" addr add "$IP_PEER/24" dev "$IFACE_PEER"
+
+    echo "$IP_RANGE" > "/tmp/nm-test-ppp-allip-$IFACE"
+
+    cat <<EOF > "/tmp/nm-test-ppp-pppoe-server-options-$IFACE"
+require-$PPP_AUTH
+lcp-echo-interval 10
+lcp-echo-failure 2
+ms-dns 8.8.8.8
+ms-dns 8.8.4.4
+netmask 255.255.255.0
+defaultroute
+noipdefault
+usepeerdns
+EOF
+
+    echo "$PPP_USER * $PPP_PASSWD $IP_PEER" > "/tmp/nm-test-ppp-$IFACE-$PPP_AUTH-secrets"
+    chmod 600 "/tmp/nm-test-ppp-$IFACE-$PPP_AUTH-secrets"
+    mkdir -p /etc/ppp
+    touch "/etc/ppp/$PPP_AUTH-secrets"
+    ip netns exec "$NETNS" bash -ex <(
+        cat <<'EOF'
+        mount -o bind  "/tmp/nm-test-ppp-$IFACE-$PPP_AUTH-secrets" "/etc/ppp/$PPP_AUTH-secrets" &&
+        exec pppoe-server \
+            -X "/tmp/nm-test-ppp-$IFACE.pid" \
+            -S "$PPP_SERVICE" \
+            -C "$PPP_SERVICE" \
+            -L "$IP_PEER" \
+            -p "/tmp/nm-test-ppp-allip-$IFACE" \
+            -I "$IFACE_PEER" \
+            -O "/tmp/nm-test-ppp-pppoe-server-options-$IFACE"
+EOF
+) &
+
+    ip --netns "$NETNS" link set "$IFACE" netns $$
+
+    nmcli connection add \
+        type pppoe \
+        con-name "$CON_NAME" \
+        ifname "ppp-$IFACE" \
+        pppoe.parent "$IFACE" \
+        service "$PPP_SERVICE" \
+        username "$PPP_USER" \
+        password "$PPP_PASSWD" \
+        autoconnect no
+}
+
+CMD="${1-setup}"
+case "$CMD" in
+    setup| \
+    cleanup)
+        "do_$CMD"
+        ;;
+    *)
+        die "invalid command $1"
+        ;;
+esac
diff --git a/contrib/scripts/test-prefix-delegation.sh b/contrib/scripts/test-prefix-delegation.sh
new file mode 100755
index 00000000..7fc4140e
--- /dev/null
+++ b/contrib/scripts/test-prefix-delegation.sh
@@ -0,0 +1,151 @@
+#!/bin/sh
+
+# Usage: ./test-prefix-delegation {ll|slaac|dhcp-stateful|dhcp-stateless}
+
+MODE=${1:-dhcp-stateful}
+
+cleanup()
+{
+    pkill -F dhcpd.pid
+    pkill -F radvd.pid
+    rm -f radvd.conf
+    rm -f dhcpd.conf
+    rm -f leases.conf
+    nmcli connection delete v1+ v2+
+    ip netns del ns1
+    ip netns del ns2
+    ip link del v1
+    ip link del v2
+}
+
+require()
+{
+    if ! command -v "$1" > /dev/null ; then
+        echo " *** Error: command '$1' not found"
+        exit 1
+    fi
+}
+
+exit_hook()
+{
+    cleanup > /dev/null 2>&1
+}
+
+require nmcli
+require ip
+require jq
+require radvd
+require dhcpd
+
+unalias ip 2> /dev/null
+
+cleanup
+trap exit_hook EXIT
+
+# ns1 is the 'upstream' namespace that provides IPv6 connectivity
+# through RA and DHCPv6. The DHCP server also acts as a delegating
+# router for /60 prefixes.
+
+# ns2 is the 'downstream' namespace where a client obtains IPv6
+# connectivity through RA from NM.
+
+# NM is in the default namespace and has a connection to ns1 with
+# ipv6.method=auto and to ns2 with ipv6.method=shared.
+
+ip netns add ns1
+ip netns add ns2
+
+ip link add v1 type veth peer name v1p
+ip link add v2 type veth peer name v2p
+
+ip link set v1p netns ns1
+ip link set v2p netns ns2
+
+ip link set v1 up
+ip link set v2 up
+
+ip -n ns1 link set v1p up
+ip -n ns1 addr add dev v1p fc01::1/64
+
+ip -n ns2 link set v2p up
+
+if [ "$MODE" = ll ]; then
+    adv_managed=off
+    adv_other=off
+elif [ "$MODE" = slaac ]; then
+    adv_managed=off
+    adv_other=off
+    adv_prefix="prefix fc01::/64 {AdvOnLink on; AdvAutonomous on; AdvRouterAddr off; };"
+elif [ "$MODE" = dhcp-stateless ]; then
+    adv_managed=off
+    adv_other=on
+    adv_prefix="prefix fc01::/64 {AdvOnLink on; AdvAutonomous on; AdvRouterAddr off; };"
+elif [ "$MODE" = dhcp-stateful ]; then
+    adv_managed=on
+    adv_other=off
+    dhcp_range="range6  fc01::1000 fc01::ffff;"
+else
+    echo "Unknown mode '$MODE'"
+    exit 1
+fi
+
+echo "Starting in $MODE mode..."
+
+cat > radvd.conf <<EOF
+interface v1p {
+        AdvSendAdvert on;
+        AdvManagedFlag ${adv_managed};
+        AdvOtherConfigFlag ${adv_other};
+        MinRtrAdvInterval 3;
+        MaxRtrAdvInterval 60;
+        ${adv_prefix}
+};
+EOF
+
+cat > dhcpd.conf <<EOF
+subnet6 fc01::/64 {
+        ${dhcp_range}
+        prefix6 fc01:bbbb:1:: fc01:bbbb:2:: / 60;
+        option dhcp6.name-servers fc01::8888;
+}
+EOF
+
+echo > leases.conf
+ip netns exec ns1 radvd -n -C radvd.conf -p radvd.pid &
+ip netns exec ns1 dhcpd -6 -d -cf dhcpd.conf -lf leases.conf -pf dhcpd.pid &
+
+nmcli connection add type ethernet ifname v1 con-name v1+ ipv4.method disabled ipv6.method auto autoconnect no
+nmcli connection add type ethernet ifname v2 con-name v2+ ipv4.method disabled ipv6.method shared autoconnect no
+
+nmcli connection up v1+
+
+sleep 5
+
+nmcli connection up v2+
+
+sleep 5
+
+ip a show dev v1
+ip a show dev v2
+
+addr=$(ip -j addr show dev v1 | jq -r '.[0].addr_info[] | select(.scope=="link")'.local)
+prefix="fc01:bbbb:1::/32"
+ip netns exec ns1 ip route add $prefix via $addr dev v1p
+
+# kernel does IPv6 autoconf in ns2 ...
+
+sleep 10
+
+# check connectivity to ns1
+if ! ip -n ns2 a show dev v2p | grep 'fc01:bbbb:[a-f0-9\:]\+/64'; then
+    ip -n ns2 a show dev v2p
+    echo "ERROR: no address"
+    exit 1
+fi
+
+if ! ip netns exec ns2 ping -c2 fc01::1; then
+    echo "ERROR: ping failed"
+    exit 1
+fi
+
+echo "OK"