summary refs log tree commit diff
path: root/src/core
diff options
context:
space:
mode:
Diffstat (limited to 'src/core')
-rw-r--r--src/core/NetworkManagerUtils.c4
-rw-r--r--src/core/README.l3cfg.md368
-rw-r--r--src/core/README.md9
-rw-r--r--src/core/README.next.ip-config.md59
-rw-r--r--src/core/devices/nm-device-bond.c4
-rw-r--r--src/core/devices/nm-device-bridge.c4
-rw-r--r--src/core/devices/nm-device-ethernet.c12
-rw-r--r--src/core/devices/nm-device-factory.c3
-rw-r--r--src/core/devices/nm-device-utils.c16
-rw-r--r--src/core/devices/nm-device-vrf.c2
-rw-r--r--src/core/devices/nm-device.c541
-rw-r--r--src/core/devices/nm-device.h15
-rw-r--r--src/core/devices/ovs/nm-device-ovs-bridge.c2
-rw-r--r--src/core/devices/ovs/nm-device-ovs-interface.c4
-rw-r--r--src/core/devices/ovs/nm-device-ovs-port.c4
-rw-r--r--src/core/devices/team/nm-device-team.c4
-rw-r--r--src/core/devices/wifi/nm-device-iwd.c4
-rw-r--r--src/core/devices/wifi/nm-device-wifi.c6
-rw-r--r--src/core/devices/wifi/nm-wifi-ap.c15
-rw-r--r--src/core/devices/wwan/meson.build3
-rw-r--r--src/core/devices/wwan/nm-modem-broadband.c64
-rw-r--r--src/core/dhcp/README.next.md103
-rw-r--r--src/core/dhcp/nm-dhcp-client.c7
-rw-r--r--src/core/dhcp/nm-dhcp-nettools.c5
-rw-r--r--src/core/dhcp/nm-dhcp-systemd.c56
-rw-r--r--src/core/meson.build2
-rw-r--r--src/core/ndisc/nm-ndisc.c1
-rw-r--r--src/core/ndisc/nm-ndisc.h2
-rw-r--r--src/core/nm-active-connection.c133
-rw-r--r--src/core/nm-active-connection.h10
-rw-r--r--src/core/nm-audit-manager.c17
-rw-r--r--src/core/nm-checkpoint.c270
-rw-r--r--src/core/nm-config-data.c47
-rw-r--r--src/core/nm-config-data.h3
-rw-r--r--src/core/nm-connectivity.h8
-rw-r--r--src/core/nm-core-utils.c3
-rw-r--r--src/core/nm-core-utils.h2
-rw-r--r--src/core/nm-l3cfg.c2
-rw-r--r--src/core/nm-manager.c171
-rw-r--r--src/core/nm-manager.h3
-rw-r--r--src/core/nm-policy.c2
-rw-r--r--src/core/nm-power-monitor.c (renamed from src/core/nm-sleep-monitor.c)162
-rw-r--r--src/core/nm-power-monitor.h34
-rw-r--r--src/core/nm-sleep-monitor.h33
-rw-r--r--src/core/nm-types.h4
-rw-r--r--src/core/settings/nm-settings-connection.c23
-rw-r--r--src/core/settings/nm-settings-connection.h4
-rw-r--r--src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-reader.c14
-rw-r--r--src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.c1
-rw-r--r--src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.h2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-writer.c8
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-ibft-dhcp4
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-ibft-static4
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-wired-8021x-peap-mschapv21
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/test-ifcfg-rh.c18
-rw-r--r--src/core/settings/plugins/keyfile/tests/test-keyfile-settings.c4
-rw-r--r--src/core/supplicant/nm-supplicant-config.c3
-rw-r--r--src/core/supplicant/nm-supplicant-settings-verify.c1
-rw-r--r--src/core/tests/config/test-config.c2
-rw-r--r--src/core/tests/test-dcb.c2
-rw-r--r--src/core/vpn/nm-vpn-connection.c116
61 files changed, 1814 insertions, 616 deletions
diff --git a/src/core/NetworkManagerUtils.c b/src/core/NetworkManagerUtils.c
index cced1717..2f447146 100644
--- a/src/core/NetworkManagerUtils.c
+++ b/src/core/NetworkManagerUtils.c
@@ -702,8 +702,8 @@ check_connection_controller(NMConnection *orig, NMConnection *candidate, GHashTa
 
     s_con_orig      = nm_connection_get_setting_connection(orig);
     s_con_cand      = nm_connection_get_setting_connection(candidate);
-    orig_controller = nm_setting_connection_get_master(s_con_orig);
-    cand_controller = nm_setting_connection_get_master(s_con_cand);
+    orig_controller = nm_setting_connection_get_controller(s_con_orig);
+    cand_controller = nm_setting_connection_get_controller(s_con_cand);
 
     /* A generated connection uses the UUID to specify the controller. Accept
      * candidates that specify as controller an interface name matching that
diff --git a/src/core/README.l3cfg.md b/src/core/README.l3cfg.md
new file mode 100644
index 00000000..901d40b4
--- /dev/null
+++ b/src/core/README.l3cfg.md
@@ -0,0 +1,368 @@
+L3Cfg Rework
+============
+
+NMDevice is complex. Together with NMManager, NMDevice does too much.
+
+The goal is to rework the IP configuration (Layer 3) to be a more separate
+part of the code that is better maintainable, easier to understand and
+extend and more correct.
+
+Current Situation
+-----------------
+
+- [NMManager](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/6b64fac06d2f6e0d9fa530ebb1ab28d53a1c5d03/src/core/nm-manager.c):
+  this is the main object (a singleton) that drives most things.
+  Among many other things, it creates NMDevice instances and coordinates.
+
+- [NMDevice](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/6b64fac06d2f6e0d9fa530ebb1ab28d53a1c5d03/src/core/devices/nm-device.c):
+  this represents a device. This is a subclass of NMDBusObject,
+  it is thus directly exported on D-Bus (as D-Bus objects like
+  `/org/freedesktop/NetworkManager/Devices/1`).
+  It also manages all aspects of the device. It has an overall state
+  (`NM_DEVICE_STATE`) but lots of more specific states (e.g. current state
+  of DHCP configuration). As always, the hardest part in programming are
+  stateful objects, and NMDevice has *a lot* of state. The code is huge and
+  hard to understand and the class has (too) many responsibilities. \
+  \
+  NMDevice also has subclasses, which are determined based on the "device type". That
+  means, there are subclasses like NMDeviceEthernet and NMDeviceBridge. As such, the
+  subclasses also export additional D-Bus interfaces. These subclasses also handle
+  the Layer 2 specific aspects of the device. For this aspect, delegation probably
+  would have been a better choice. On the other hand, IP configuration is almost entirely
+  managed by the parent class. Which is good, because the IP configuration is common to all
+  device types, but is bad because NMDevice already does so many things.
+
+- [NMIP4Config](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/6b64fac06d2f6e0d9fa530ebb1ab28d53a1c5d03/src/core/nm-ip4-config.c) (and NMIP6Config):
+  these are also subclasses of NMDBusObject
+  and exported on D-Bus on paths like `/org/freedesktop/NetworkManager/IP4Config/1`.
+  The device's `IP4Config` property refers to these objects. They contain
+  the runtime IP information of that device. I don't think these objects
+  should exist on the D-Bus API, as NMDevice could directly expose these properties.
+  But for historic reasons, such is our D-Bus API.
+  Other than that, NMIP4Config objects are also used internally for tracking
+  IP configuration. For example, [when](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/6b64fac06d2f6e0d9fa530ebb1ab28d53a1c5d03/src/core/dhcp/nm-dhcp-nettools.c#L563)
+  we receive a DHCP lease, we construct a NMIP4Config object with the addresses, DNS settings,
+  and so on. These
+  instances are then [tracked by](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/6b64fac06d2f6e0d9fa530ebb1ab28d53a1c5d03/src/core/devices/nm-device.c#L519)
+  NMDevice, and [merged](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/6b64fac06d2f6e0d9fa530ebb1ab28d53a1c5d03/src/core/devices/nm-device.c#L8928)
+  into an instance that is then exposed on D-Bus. As such, this class has two
+  mostly independent purposes.
+
+- [NMDhcpClient](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/6b64fac06d2f6e0d9fa530ebb1ab28d53a1c5d03/src/core/dhcp/nm-dhcp-client.c):
+  our DHCP "library". It's a simple object with a clear API that
+  abstracts most of the complexity of handling DHCP. But still, NMDevice
+  needs to drive the DHCP client instance. Meaning, it needs to create (start) and stop
+  them and hook up signals for changes (new lease) and timeout. This is mostly
+  fine and unavoidable. The point is that while specific tasks are well abstracted
+  (like the details of DHCP), there is still some state in NMDevice that is related
+  to manage these tasks. DHCP is one of many such tasks, like also
+  link local addresses, SLAAC or LLDP.
+  This leads to the increased complexity of NMDevice, which manages a large variety
+  of such tasks.
+
+### Problems:
+
+1. first the sheer code size of [nm-device.c](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/6b64fac06d2f6e0d9fa530ebb1ab28d53a1c5d03/src/core/devices/nm-device.c#L19030).
+   It's hard to understand and maintain, and this results in misbehaviours. Also, features that should be easy to implement
+   are not. Also, there are inefficiencies that are hard to fix.
+
+2. NMDevice and NMIP4Config are both exported on D-Bus while having other responsibilities.
+   Being subclasses of NMDBusObject, they are glued to the D-Bus API. For example, NMIP4Config is
+   also used for other purposes (for tracking IP configuration internally).
+
+3. NMDevice simply does too much. IP configuration should be a separate, encapsulated
+   API to make allow NMDevice to be smaller and the IP configuration part better
+   testable, understandable and smaller too.
+
+4. in the current model, NMDevice can be related to zero, one or two ifindexes. For example,
+   for ethernet devices, there is commonly only one actual netdev device (and one ifindex).
+   For OVS devices, there is no ifindex. For NMDeviceModem or NMDeviceBluetooth there is
+   a NMDevice instance that has initially no ifindex (it represents the tty serial port
+   or the bluetooth device) but during activation it gets and ip ifindex. With PPPoE,
+   the ethernet device can even have two ifindexes (one for the underlying ethernet and
+   one for the PPP device). That is all utterly confusing, inconsistent and limited.
+   For example, not all interfaces you see in `ip link` can be found in the D-Bus API.
+   The D-Bus API also does not give access to the ifindex (which is the real identifier
+   for a netdev devices). It only exposes the IpInterface name. That should be improved too,
+   but even such seemingly simple things are not done for years, because it's not trivially
+   clear what the right ifindex is.
+   Also a device instance on D-Bus significantly changes its meaning when it activates/deactivates
+   and it starts/stops being responsible for an ifindex.
+   In the future there should be devices that represent exactly one netdev device (an ifindex)
+   and devices that don't have an ifindex. That is follow up work and hinted by
+   [rhbz#1066703](https://bugzilla.redhat.com/show_bug.cgi?id=1066703). But simplifying
+   the IP configuration is a requisite before addressing that rework.
+   With this we will have controller and controlled devices. That means, a controller devices
+   (that for example represents a bluetooth device) will need to configure IP address on the
+   controlled IP device. That would be doable by injecting the IP config on that device,
+   but as the device already does so much, it would be better if this would be a separate
+   IP configuration manager for that ifindex.
+
+5. NMIP4Config exports properties on D-Bus like [AddressData](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/6b64fac06d2f6e0d9fa530ebb1ab28d53a1c5d03/introspection/org.freedesktop.NetworkManager.IP4Config.xml#L26).
+   which are the currently configured IP addresses. These should be directly obtained
+   from the NMPlatform cache, which contains the correct list of addresses as kernel
+   exposes them via rtnetlink. Instead, whenever there are changes in platform we
+   [generate](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/6b64fac06d2f6e0d9fa530ebb1ab28d53a1c5d03/src/core/devices/nm-device.c#L14223)
+   an NMIP4Config instance, then we merge, intersect and subtract this captured information
+   with the IP configs we want to configure. Finally we merge them together again
+   and sync the result to platform. This is bad, wrong and inefficient.
+   We must not mix "what is configured" with "what we want to configure". The current
+   approach also re-generates these IP config instance whenever something in platform changes.
+   That does not scale. If we have any hope to handle thousands of routes, this needs to change.
+
+6. The NMIP4Config objects are mutable, and they are heavily mutated. When we create an NMIP4Config
+   instance that represent a DHCP lease, we will [subtract](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/6b64fac06d2f6e0d9fa530ebb1ab28d53a1c5d03/src/core/devices/nm-device.c#L14236)
+   addresses that were externally removed. That is wrong, because during a reapply we
+   will need to know these addresses again. The solution for that is not to mutate this
+   data, but track whether IP addresses are removed separately.
+
+7. NMDevice also does ACD, but it can only do it for addresses received via DHCP.
+   It implicitly also does ACD for IPv4LL, but that is via using the n_ipv4ll library.
+   It would be good to have an option that we can configure IPv4LL for any address.
+   Also, if you manually configure an address like 192.168.2.5 (for which we don't do
+   ACD) and the same address is obtained via DHCP, then doing ACD for the address is wrong.
+   There needs to be link-wide view of the addresses, and not only looking at individual
+   addresses when deciding to do ACD.
+
+8. As IP configuration is done by NMDevice, VPN connections have limited capabilities
+   in this regard.
+   When a VPN has IP addresses, then it injects them into NMDevice by
+   [providing](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/6b64fac06d2f6e0d9fa530ebb1ab28d53a1c5d03/src/core/devices/nm-device.c#L13696)
+   an NMIP4Config. However, that means VPNs cannot do DHCP or IPv4LL, because it can
+   only inject known configuration. That would be very useful for example with a tap
+   device with openvpn. The real problem here is that NMVpnConnection are
+   treated special, when they should be more like devices. That should be reworked in the future,
+   by reworking VPN plugins. Regardless, having IP configuration handled by NMDevice is limiting.
+
+9. NetworkManager currently supports `ipv4.method` which can be "manual", "disabled" or
+   "auto". This scheme does not allow for example to enable IPv4LL together with DHCPv4.
+   As a special case, you can configure `ipv4.method=auto` together with static
+   addresses in `ipv4.addresses`, so combining DHCP and static addressing works. But in general,
+   this scheme is limited. In the future we should have more flexible schemes, where
+   addressing methods can be independently enabled or disabled. Also, we currently
+   have `ipv4.may-fail`, but that is limited as well. For example,
+   `ipv4.may-fail=yes` and `ipv6.may-fail=yes` still means that at least one of the
+   address families must succeed. That makes sense for certain use cases, but it
+   means, you cannot have truly best-effort, opportunistic DHCP with this way.
+   As workaround for that there is `ipv4.dhcp-timeout=infinity`. In
+   general it is not only useful to enable methods independently, we also configure
+   independently whether they are required or optional (and possibly, that they are optional
+   but at least one of several optional methods must succeed). Anyway. The point
+   is there is a need to make IP configuration more flexible. Currently it is not.
+   Such a seemingly simple extension would be surprisingly difficult to implement
+   because [the code](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/6b64fac06d2f6e0d9fa530ebb1ab28d53a1c5d03/src/core/devices/nm-device.c#L6616) is
+   all over the place. The way how NMDevice tracks the overall activation state is
+   hard to understand. This should be improved and possibly could be improved in a
+   smaller refactoring effort. But instead of a smaller effort, we will use the big hammer
+   with L3Cfg rework.
+
+10. There are two classes NMIP4Config and NMIP6Config. Handling both address families is
+   commonly similar, so there is lot of similar code in both. They should be unified
+   so that similar code can handle both address families.
+
+
+Solution and Future
+-------------------
+
+NML3Cfg work is supposed to simplify some part of NMDevice: the part related to
+IP configuration. This is a huge rework of a core part of NetworkManager. Arguably,
+some parts maybe could be done more evolutionary, but the fundamental problems require
+to rip out NMIP4Config and replace it by something better. Doing that is a large rework
+that changes NMDevice heavily. That is also the opportunity to get the smaller issues
+right.
+
+There is already a new class [NML3Cfg](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/6b64fac06d2f6e0d9fa530ebb1ab28d53a1c5d03/src/core/nm-l3cfg.h#L141)
+(currently unused). An NML3Cfg instance is responsible for handling IP configuration
+of an ifindex. Consequently, we can ask NMNetns to [get](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/6b64fac06d2f6e0d9fa530ebb1ab28d53a1c5d03/src/core/nm-netns.c#L142)
+(or create) a NML3Cfg instance for an ifindex.
+The idea is that there can be multiple users (NMDevice and NMVpnConnection and future controller devices)
+that use the same NML3Cfg instance. Especially with a future rework of NMDevice where
+a NMDevice only manages one ifindex (or none), there is a need that multiple
+devices manage the IP configuration on the same device. Independent users can cooperate
+to configure IP configuration on the same device. Already now with Libreswan VPN, where the VPN "injects"
+its NMIP4Config in NMDevice. Or with PPPoE, where the NMDeviceEthernet is both about IP configuration
+for the PPPoE device.
+
+There is also a new class [NML3ConfigData](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/6b64fac06d2f6e0d9fa530ebb1ab28d53a1c5d03/src/core/nm-l3-config-data.h).
+This replaces some aspect of NMIP4Config/NMIP6Config. A NML3ConfigData object is immutable and has no real logic
+(or state). It has some "logic", like comparing two NML3ConfigData instances, logging it, or merging two (immutable)
+instances into a new instance. But as the class is immutable, that logic is rather simple. This class is
+used to track information. As it's immutable, anybody who is interested can keep a reference
+for it's own purpose. For example, NMDhcpClient will generate a NML3ConfigData with the information
+of the lease. It may keep the reference, but it will also tell NMDevice about it. The NMDevice
+will then itself tell NML3Cfg to accept this configuration. This works by calling
+[add()/remove()](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/6b64fac06d2f6e0d9fa530ebb1ab28d53a1c5d03/src/core/nm-l3cfg.c#L2654).
+One NML3ConfigData can also track both IPv4 and IPv6 information. It's a general set of IP related
+configuration, that has some address specific properties. Those are then duplicated for both address
+families and implemented in a way to minimize code duplication and encourage to treat them the same.
+As this replaces an aspect of NMIP4Config, NMIP4Config can focus on it's other purpose: to expose data on D-Bus.
+
+What NML3Cfg then does, is to merge all NML3ConfigData, and "commit" it to platform. Thereby it knows
+which addresses it configured the last time (if they no longer are to be configured, they must be removed).
+This is done [here](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/6b64fac06d2f6e0d9fa530ebb1ab28d53a1c5d03/src/core/nm-l3cfg.c#L3442).
+
+As independent users should be able to cooperate, it is not appropriate that they call "commit".
+Instead, they set a commit type ([here](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/6b64fac06d2f6e0d9fa530ebb1ab28d53a1c5d03/src/core/nm-l3cfg.c#L3476),
+and whenever something changes, NML3Cfg knows the aggregated commit type. That is necessary
+because when we activate a device, we may want to preserve the existing IP configuration (e.g. after
+a restart of NetworkManager). During that time is the NML3Cfg instance set to a reduced commit
+mode (ASSUME).
+
+NML3Cfg will also handle IPv4 ACD. Any user of NML3Cfg registers/unregisters NML3ConfigData instances
+that should be configured. Thereby they also say whether ACD should be done for the IPv4 addresses.
+NML3Cfg then keeps state for each IPv4 address, whether ACD should be performed, and whether the
+address is ready to be configured. NML3Cfg does not do DHCP or similar. That is still the responsibility
+of NMDevice to run a NMDhcpClient. But it does run ACD, because whether to perform ACD on an address
+requires a holistic view of all addresses of that interface. For example, if you configure a static
+IP address 192.168.2.5 (with ACD disabled) and you also get the same address via DHCP, then ACD should
+not performed for that address (even if the user configured ACD with DHCP). Of course, that is a very
+unlikely example. More likely is that NetworkManager is restarted and it leaves the addresses (that passed
+ACD) configured. After restart, DHCP finds the same addresses and no new ACD should be performed. This shows
+that the ACD state depends all the IP addresses on an interface,
+and thus it's done by NML3Cfg. The API for this is very simple. Users enable/disable ACD during nm_l3cfg_add_config()
+and receive events like [NM_L3_CONFIG_NOTIFY_TYPE_ACD_EVENT](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/6b64fac06d2f6e0d9fa530ebb1ab28d53a1c5d03/src/core/nm-l3cfg.c#L303).
+Another advantage is that ACD now works for any kinds of addresses. Currently it only works for addresses
+from DHCP and link local addresses.
+
+NML3Cfg does not implement or drive DHCP. However, as it already does ACD it gained it's own IPv4LL
+"library": [NML3IPv4LL](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/6b64fac06d2f6e0d9fa530ebb1ab28d53a1c5d03/src/core/nm-l3cfg.c#L3624).
+This will replace nettools' n-ipv4ll library, because that library also does ACD internally, while we want
+to use the holistic view that NML3Cfg has. What this means, is that the user (NMDevice)
+can request a NML3IPv4LL handle from the NML3Cfg instance, and it just does it with a simple API.
+All the user might do is to enable/disable the handle and to react to signals (if it cares to find
+out whether IPv4LL fails).
+
+The general parts of NML3Cfg are already implemented. It has unit tests and can be tested independently.
+You might note that NML3Cfg is not trivial already, but the API that it provides is as simple as possible:
+create immutable NML3ConfigData instance, and add/remove them. Optionally, handle the ACD events and
+listen to some events. The complexity that NML3Cfg has, will lead in the same amount simplify NMDevice.
+
+What is missing is NMDevice using this new API. Instead of creating and tracking NMIP4Config instances,
+it needs to track NML3ConfigData instances. In principle that sounds simple, in practice that changes
+large part of "nm-device.c".
+
+Thereby also the state machine for NM_DEVICE_STATE will be improved. It's anyway a rewrite. This will lay the
+groundwork for more flexible configuration of IP methods, with different failure modes (opportunistic or
+mandatory).
+
+What then also should be easier, to combine IPv4LL with other addressing methods. In Windows AFAIK, if you
+don't get a DHCP address it will configure a IPv4LL address. That is also what RFC suggests, but which we
+currently don't support.
+
+In general, change the way how external IP addresses/routes are tracked. This merge, intersect, subtract
+approach does not perform well. Currently we react on signals and it's hard to understand what happens
+in response to that, or whether it's really the correct thing to do. See yourself starting from
+[here](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/6b64fac06d2f6e0d9fa530ebb1ab28d53a1c5d03/src/core/devices/nm-device.c#L14214).
+
+### DHCP
+
+Currently, when NMDhcpClient receives a lease, it emits a signal with two things: the NMIP4Config
+instance (containing addresses, routes, DNS settings and other information for later use), and a string
+dictionary with the DHCP lease options (they are mainly used to expose them on D-Bus). The latter is
+immutable (meaning, it's not changed afterwards). That does not significantly change with L3Cfg. The
+difference is that instead of NMIP4Config a NML3ConfigData instance gets created. That instance then
+references the (immutable) strdict. With that, any part of the code that has access to the NML3ConfigData,
+also has access to the lease options. So instead of two separate
+pieces of information, the result of a lease event will only be a NML3ConfigData instance (which internally
+tracks the strdict with the DHCP lease options).
+
+Later, when NML3Cfg configures an interface, it takes all NML3ConfigData instances that were added to
+it, and merges them. [Currently](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/6b64fac06d2f6e0d9fa530ebb1ab28d53a1c5d03/src/core/nm-l3-config-data.c#L2693),
+the merged data will not contain the lease information, but it's probably not needed anyway.
+
+If it would be needed, the question is what happens if multiple lease informations are present
+during the merge. Duplicate leases would not commonly happen, but in general, the merging algorithm
+needs to take into account priorities and conflicting data.
+That is done by users who call [add](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/6b64fac06d2f6e0d9fa530ebb1ab28d53a1c5d03/src/core/nm-l3cfg.c#L2658)
+to provide a priority for the NML3ConfigData instance.
+Later, the instances get sorted by priority and merging is smart to take that into account
+([here](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/6b64fac06d2f6e0d9fa530ebb1ab28d53a1c5d03/src/core/nm-l3cfg.c#L2983)).
+
+Also, we currently inject the route-metric and table into the generated NMIP4Config.
+Those settings come from the connection profiles and not from DHCP. We will avoid that
+by allowing the routes in NML3ConfigData to be marked as metric\_any and table\_any.
+That way,the NML3ConfigData is independent (and immutable) with respect to those settings.
+The same happens for example with PPP, where the modem starts PPP, and currently the
+route and metric needs to be passed several layers down. But worst, those settings
+can change during reapply. Currently that means we need to hack NMIP4Config with
+those changes. Later, we will only tell NML3Cfg to track the NML3ConfigData with
+different settings.
+
+### DNS
+
+DNS information is currently set in the NMIP4Config instances. That happens for example with the DNS information
+from a DHCP lease, but also with the static DNS settings from the connection profile. Later, the same information
+will packed in NML3ConfigData.
+
+One nice difference is again the immutability. Currently, NMDnsManager keeps a reference to all relevant NMIP4Config instances,
+but as they are mutable, it needs to [subscribe](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/6b64fac06d2f6e0d9fa530ebb1ab28d53a1c5d03/src/core/dns/nm-dns-manager.c#L275)
+to changes. Later, when a NML3ConfigData instance "changes", it means it was
+replaced by a different one and NMDnsManager needs to update its list of tracked NML3ConfigData. I find that
+cleaner, because adding and removal to the list of NMIP4Config/NML3ConfigData happens anyway and needs to be handled.
+
+
+Related Bugs
+------------
+
+* Main bug:
+
+  - [rh#1868254](https://bugzilla.redhat.com/show_bug.cgi?id=1868254):
+    "refactor NetworkManager's IP configuration done by NMDevice"
+
+* Follow up but to improve model of devices:
+
+  - [rh#1066703](https://bugzilla.redhat.com/show_bug.cgi?id=1066703):
+    "\[RFE\] Handle parent/child relationships more cleanly"
+
+* Flexible IP methods:
+
+  - [rh#1791624](https://bugzilla.redhat.com/show_bug.cgi?id=1791624):
+    "NetworkManager must not remove used bridge"
+
+* Improving performance issues, this will lay ground work:
+
+  - [rh#1847125](https://bugzilla.redhat.com/show_bug.cgi?id=1847125):
+    "\[RFE\] Improve 20% performance on creating 1000 bridge over 1000 VLANs"
+
+  - [rh#1861527](https://bugzilla.redhat.com/show_bug.cgi?id=1861527):
+    "Excessive memory and CPU usage on a router with IPv6 BGP feed"
+
+  - [rh#1753677](https://bugzilla.redhat.com/show_bug.cgi?id=1753677):
+    "High cpu usage while non-controlled interface is mangling tc filters"
+
+TODO
+----
+
+- Before considering an IP method completely activated, check that all addresses
+  we want to configure are no longer tentative.
+  For example, when activating a `connection.type=pppoe`, we might get an IPv6 address
+  from the pppd daemon. We set that via
+  `nm_device_devip_set_state(device, addr_family, NM_DEVICE_IP_STATE_READY, ip_data->l3cd)`
+  Note that this currently already is sufficient to make the IP method (as far as PPP
+  is concerned), to be "ready".
+  We should however wait for IPv6 DAD to complete. See how that is already done
+  for ipmanual (`_dev_ipmanual_check_ready()`).
+
+- NMDevicePPP fails to re-activate a profile that is currently active. This is not
+  new on "next" branch, but also happened before. Have a pppoe profile active,
+  and issue `nmcli connection up $PROFILE` again. This, as expected, brings down
+  the device but fails to re-activate it. It's not trivial to fix (I think), because
+  of the ip-ifindex madness. Check the logs. Test with contrib/scripts/test-ppp.sh
+  script. Possibly affects other types.
+
+- NMDevice's ip4_config_pre_commit() had only one effect, to call nm_modem_ip4_pre_commit()
+  which set IFF_NOARP. That is currently dropped. Find a different way to achieve that.
+  Theoretically, we still could do that from NML3Cfg's post-commit notification. But
+  this really should be handled by NML3Cfg. That means, to disable IFF_NOARP needs somehow
+  configured by NMDeviceModem -- possibly by setting a flag in NML3ConfigData that indicates
+  to do this. Or maybe NML3Cfg should detect automatically when to set IFF_NOARP.
+
+- `ipvx.method=auto` usually means autoconf6/DHCPv4, but it doesn't have to. For example
+  with PPP/VPN it might mean that the IP configuration is provided by pppd/VPN or with
+  Wi-Fi-P2P it might mean that the controller of a peer runs a DHCP server (shared).
+  Now the parents implementation activate_stage3_ip_config() can no longer be overwritten
+  by subclasses, and they always perform their steps. For subclasses to modify what a
+  method means there is klass->get_ip_method_auto(self(). NMDeviceModem does not yet
+  implement that, which is most certainly lacking. We need to test at least NMDeviceModem
+  whether IP methods work correctly in this regard. Possibly other subclasses are affected
+  too (NMDeviceBluetooth?).
diff --git a/src/core/README.md b/src/core/README.md
new file mode 100644
index 00000000..13779197
--- /dev/null
+++ b/src/core/README.md
@@ -0,0 +1,9 @@
+core
+====
+
+The source code of the NetworkManager daemon.
+
+NetworkManager is a daemon that provides a D-Bus API and a file-based
+API for configuring the network on a Linux host.
+
+This is the daemon source code.
diff --git a/src/core/README.next.ip-config.md b/src/core/README.next.ip-config.md
new file mode 100644
index 00000000..e5be74f1
--- /dev/null
+++ b/src/core/README.next.ip-config.md
@@ -0,0 +1,59 @@
+Rework `NMIP[46]Config` for `next` branch
+=========================================
+
+The `next` branch is a large rework of internals, how IP configuration is done by `NMDevice`.
+
+Previously, there are two `GObject`s named `NMIP4Config` and `NMIP6Config`. These
+serve different purposes:
+
+1) They are data containers that can track IP configuration. As such, `NMDevice`
+   and various parts (like `NMDhcpClient`) create them, pass them around and
+   mutate/merge them to track the IP configuration.
+
+2) They are also subclasses of `NMDBusObject` and exported on D-Bus as
+   `/org/freedesktop/NetworkManager/IP4Config/1`, etc. As such, see their
+   [D-Bus API](../../introspection/org.freedesktop.NetworkManager.IP4Config.xml)
+   (and [for IPv6](../../introspection/org.freedesktop.NetworkManager.IP6Config.xml)).
+
+`next` branch will replace use 1) with `NML3ConfigData`. `NML3ConfigData` are immutable
+(sealable) data containers with little logic. This leaves `NMIP4Config` to only
+implement 2).
+
+This needs to be reworked.
+
+* Now `NMIP4Config` and `NMIP6Config` are subclasses of `NMIPConfig`. The goal
+  is to treat IPv4/IPv6 similar and generically. Probably there should be very
+  little code in the subclasses left and most should move to the parent classes.
+  We still need separate GObject types though, because that is how `NMDBusObject`'s
+  glue code can handle different D-Bus paths.
+
+* Now `NML3Cfg` is a handle for the IP configuration parameters of a device (ifindex).
+  As `NMIPConfig` mostly is about exporting the current IP configuration, it probably
+  can get most of it from there (and by listening to signals to that).
+
+* Note that `NMDevice`, `NMActiveConnection` refer `NMIP[46]Config`s, and most
+  importantly, the respective D-Bus objects refer to them. As `NMVpnConnection`
+  (and "org.freedesktop.NetworkManager.VPN.Connection" interface) are modeled
+  as "subclasses" of `NMActiveConnection`, they also have one. That means,
+  it's not entirely clear what these properties even are. For example, currently,
+  `NMDevice` does a (terrible) dance of tracking external `NMIP[46]Config` objects,
+  merging, intersecting and subtracting them with other `NMIP4Config` objects
+  to get the merged one, which is then exported on D-Bus. That merged object
+  does therefore not directly expose the IP addresses that are actually
+  configured on the interface (`ip addr`), but more what NetworkManager
+  wanted to configure and the (terrible) feedback loop where the platform
+  addresses get synced. With `next` branch and `NML3Cfg` there is a clear distinction
+  between what NetworkManager wants to configure vs. what is actually configured.
+  I think for `NMDevice` and `NMActiveConnection`, the IP addresses on
+  "org.freedesktop.NetworkManager.IP4Config" should expose the IP addresses
+  that are actually in platform (`ip addr`). If there is a need to expose
+  additional information (like things that NetworkManager wanted to configure),
+  then this should be different/new API.
+  On the other hand, currently `NMVpnConnection`'s `NMIP4Config` only tracks the 
+  IP addresses that come from the VPN plugin. So it's much more what it wants
+  to configure (from the VPN plugin), and not at all about what is configured
+  on the interface.
+  I think that needs to change. A `NMIPConfig` object on D-Bus exposes IP configuration
+  information about an netdev interface. Period. That also means that a `NMVpnConnection`
+  (which currently is like a active connection associated with the device) links to
+  the same `NMIPConfig` object as the underlying device.
diff --git a/src/core/devices/nm-device-bond.c b/src/core/devices/nm-device-bond.c
index a3467930..98670c8d 100644
--- a/src/core/devices/nm-device-bond.c
+++ b/src/core/devices/nm-device-bond.c
@@ -922,7 +922,7 @@ deactivate(NMDevice *device)
 static void
 nm_device_bond_init(NMDeviceBond *self)
 {
-    nm_assert(nm_device_is_master(NM_DEVICE(self)));
+    nm_assert(nm_device_is_controller(NM_DEVICE(self)));
 }
 
 static const NMDBusInterfaceInfoExtended interface_info_device_bond = {
@@ -958,7 +958,7 @@ nm_device_bond_class_init(NMDeviceBondClass *klass)
     device_class->connection_type_check_compatible = NM_SETTING_BOND_SETTING_NAME;
     device_class->link_types                       = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_BOND);
 
-    device_class->is_master                = TRUE;
+    device_class->is_controller            = TRUE;
     device_class->get_generic_capabilities = get_generic_capabilities;
     device_class->complete_connection      = complete_connection;
 
diff --git a/src/core/devices/nm-device-bridge.c b/src/core/devices/nm-device-bridge.c
index ab3a6be9..2405beea 100644
--- a/src/core/devices/nm-device-bridge.c
+++ b/src/core/devices/nm-device-bridge.c
@@ -1159,7 +1159,7 @@ reapply_connection(NMDevice *device, NMConnection *con_old, NMConnection *con_ne
 static void
 nm_device_bridge_init(NMDeviceBridge *self)
 {
-    nm_assert(nm_device_is_master(NM_DEVICE(self)));
+    nm_assert(nm_device_is_controller(NM_DEVICE(self)));
 }
 
 static const NMDBusInterfaceInfoExtended interface_info_device_bridge = {
@@ -1194,7 +1194,7 @@ nm_device_bridge_class_init(NMDeviceBridgeClass *klass)
     device_class->connection_type_supported = NM_SETTING_BRIDGE_SETTING_NAME;
     device_class->link_types                = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_BRIDGE);
 
-    device_class->is_master                   = TRUE;
+    device_class->is_controller               = TRUE;
     device_class->mtu_force_set               = TRUE;
     device_class->get_generic_capabilities    = get_generic_capabilities;
     device_class->check_connection_compatible = check_connection_compatible;
diff --git a/src/core/devices/nm-device-ethernet.c b/src/core/devices/nm-device-ethernet.c
index 0f7f9d65..8c2a9643 100644
--- a/src/core/devices/nm-device-ethernet.c
+++ b/src/core/devices/nm-device-ethernet.c
@@ -370,7 +370,7 @@ check_connection_compatible(NMDevice     *device,
     if (s_wired) {
         const char        *mac, *perm_hw_addr;
         gboolean           try_mac = TRUE;
-        const char *const *mac_blacklist;
+        const char *const *mac_denylist;
         int                i;
 
         if (!match_subchans(self, s_wired, &try_mac)) {
@@ -390,17 +390,17 @@ check_connection_compatible(NMDevice     *device,
                 return FALSE;
             }
 
-            /* Check for MAC address blacklist */
-            mac_blacklist = nm_setting_wired_get_mac_address_blacklist(s_wired);
-            for (i = 0; mac_blacklist[i]; i++) {
-                if (!nm_utils_hwaddr_valid(mac_blacklist[i], ETH_ALEN)) {
+            /* Check for MAC address denylist */
+            mac_denylist = nm_setting_wired_get_mac_address_denylist(s_wired);
+            for (i = 0; mac_denylist[i]; i++) {
+                if (!nm_utils_hwaddr_valid(mac_denylist[i], ETH_ALEN)) {
                     nm_utils_error_set_literal(error,
                                                NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
                                                "invalid MAC in blacklist");
                     return FALSE;
                 }
 
-                if (nm_utils_hwaddr_matches(mac_blacklist[i], -1, perm_hw_addr, -1)) {
+                if (nm_utils_hwaddr_matches(mac_denylist[i], -1, perm_hw_addr, -1)) {
                     nm_utils_error_set_literal(error,
                                                NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
                                                "permanent MAC address of device blacklisted");
diff --git a/src/core/devices/nm-device-factory.c b/src/core/devices/nm-device-factory.c
index 69c2a38f..22c8fa5a 100644
--- a/src/core/devices/nm-device-factory.c
+++ b/src/core/devices/nm-device-factory.c
@@ -183,7 +183,8 @@ nm_device_factory_class_init(NMDeviceFactoryClass *klass)
 static GHashTable *factories_by_link    = NULL;
 static GHashTable *factories_by_setting = NULL;
 
-static void __attribute__((destructor)) _cleanup(void)
+static void __attribute__((destructor))
+_cleanup(void)
 {
     nm_clear_pointer(&factories_by_link, g_hash_table_unref);
     nm_clear_pointer(&factories_by_setting, g_hash_table_unref);
diff --git a/src/core/devices/nm-device-utils.c b/src/core/devices/nm-device-utils.c
index ed0a2738..80909805 100644
--- a/src/core/devices/nm-device-utils.c
+++ b/src/core/devices/nm-device-utils.c
@@ -128,8 +128,20 @@ NM_UTILS_LOOKUP_STR_DEFINE(
     NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_SRIOV_CONFIGURATION_FAILED,
                              "sriov-configuration-failed"),
     NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_PEER_NOT_FOUND, "peer-not-found"),
-    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_DEVICE_HANDLER_FAILED,
-                             "device-handler-failed"), );
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_DEVICE_HANDLER_FAILED, "device-handler-failed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_UNMANAGED_BY_DEFAULT, "unmanaged-by-default"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_UNMANAGED_EXTERNAL_DOWN,
+                             "unmanaged-external-down"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_UNMANAGED_LINK_NOT_INIT,
+                             "unmanaged-link-not-init"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_UNMANAGED_QUITTING, "unmanaged-quitting"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_UNMANAGED_SLEEPING, "unmanaged-sleeping"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_UNMANAGED_USER_CONF, "unmanaged-user-conf"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_UNMANAGED_USER_EXPLICIT,
+                             "unmanaged-user-explicit"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_UNMANAGED_USER_SETTINGS,
+                             "unmanaged-user-settings"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_UNMANAGED_USER_UDEV, "unmanaged-user-udev"), );
 
 NM_UTILS_LOOKUP_STR_DEFINE(nm_device_mtu_source_to_string,
                            NMDeviceMtuSource,
diff --git a/src/core/devices/nm-device-vrf.c b/src/core/devices/nm-device-vrf.c
index a13de1cb..6c7adc6c 100644
--- a/src/core/devices/nm-device-vrf.c
+++ b/src/core/devices/nm-device-vrf.c
@@ -329,7 +329,7 @@ nm_device_vrf_class_init(NMDeviceVrfClass *klass)
 
     device_class->connection_type_supported        = NM_SETTING_VRF_SETTING_NAME;
     device_class->connection_type_check_compatible = NM_SETTING_VRF_SETTING_NAME;
-    device_class->is_master                        = TRUE;
+    device_class->is_controller                    = TRUE;
     device_class->link_types                       = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_VRF);
 
     device_class->attach_port                 = attach_port;
diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c
index 34022efb..5b377529 100644
--- a/src/core/devices/nm-device.c
+++ b/src/core/devices/nm-device.c
@@ -95,6 +95,9 @@
 #define CARRIER_WAIT_TIME_MS             6000
 #define CARRIER_WAIT_TIME_AFTER_MTU_MSEC 10000
 
+#define SECONDS_PER_WEEK 604800
+#define SECONDS_PER_DAY  86400
+
 #define NM_DEVICE_AUTH_RETRIES_UNSET    -1
 #define NM_DEVICE_AUTH_RETRIES_INFINITY -2
 #define NM_DEVICE_AUTH_RETRIES_DEFAULT  3
@@ -371,7 +374,6 @@ NM_GOBJECT_PROPERTIES_DEFINE(NMDevice,
                              PROP_IFINDEX,
                              PROP_AVAILABLE_CONNECTIONS,
                              PROP_PHYSICAL_PORT_ID,
-                             PROP_MASTER,
                              PROP_PARENT,
                              PROP_HW_ADDRESS,
                              PROP_PERM_HW_ADDRESS,
@@ -386,7 +388,8 @@ NM_GOBJECT_PROPERTIES_DEFINE(NMDevice,
                              PROP_IP4_CONNECTIVITY,
                              PROP_IP6_CONNECTIVITY,
                              PROP_INTERFACE_FLAGS,
-                             PROP_PORTS, );
+                             PROP_PORTS,
+                             PROP_CONTROLLER, );
 
 typedef struct _NMDevicePrivate {
     guint device_link_changed_id;
@@ -707,10 +710,10 @@ typedef struct _NMDevicePrivate {
         NMPlatformLinkChangeFlags flags;
     } link_props_state;
 
-    /* master interface for bridge/bond/team slave */
-    NMDevice *master;
-    gulong    master_ready_id;
-    int       master_ifindex;
+    /* controller interface for bridge/bond/team slave */
+    NMDevice *controller;
+    gulong    controller_ready_id;
+    int       controller_ifindex;
 
     /* slave management */
     CList slaves; /* list of SlaveInfo */
@@ -1523,11 +1526,12 @@ _prop_get_ipvx_route_table(NMDevice *self, int addr_family)
     if (route_table == 0u && connection
         && (s_con = nm_connection_get_setting_connection(connection))
         && (nm_streq0(nm_setting_connection_get_port_type(s_con), NM_SETTING_VRF_SETTING_NAME)
-            && priv->master && nm_device_get_device_type(priv->master) == NM_DEVICE_TYPE_VRF)) {
+            && priv->controller
+            && nm_device_get_device_type(priv->controller) == NM_DEVICE_TYPE_VRF)) {
         const NMPlatformLnkVrf *lnk;
 
         lnk = nm_platform_link_get_lnk_vrf(nm_device_get_platform(self),
-                                           nm_device_get_ifindex(priv->master),
+                                           nm_device_get_ifindex(priv->controller),
                                            NULL);
 
         if (lnk)
@@ -2269,6 +2273,7 @@ _prop_get_ipv4_dhcp_vendor_class_identifier(NMDevice *self, NMSettingIP4Config *
 static NMSettingIP6ConfigPrivacy
 _prop_get_ipv6_ip6_privacy(NMDevice *self)
 {
+    NMDevicePrivate          *priv = NM_DEVICE_GET_PRIVATE(self);
     NMSettingIP6ConfigPrivacy ip6_privacy;
     NMConnection             *connection;
 
@@ -2302,16 +2307,100 @@ _prop_get_ipv6_ip6_privacy(NMDevice *self)
     if (!nm_device_get_ip_ifindex(self))
         return NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN;
 
-    /* 3.) No valid default-value configured. Fallback to reading sysctl.
-     *
-     * Instead of reading static config files in /etc, just read the current sysctl value.
-     * This works as NM only writes to "/proc/sys/net/ipv6/conf/IFNAME/use_tempaddr", but leaves
-     * the "default" entry untouched. */
-    ip6_privacy = nm_platform_sysctl_get_int32(
-        nm_device_get_platform(self),
-        NMP_SYSCTL_PATHID_ABSOLUTE("/proc/sys/net/ipv6/conf/default/use_tempaddr"),
-        NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN);
-    return _ip6_privacy_clamp(ip6_privacy);
+    /* 3.) No valid default value configured. Fall back to the original value
+     * from before NM started. */
+    return _ip6_privacy_clamp(_nm_utils_ascii_str_to_int64(
+        g_hash_table_lookup(priv->ip6_saved_properties, "use_tempaddr"),
+        10,
+        G_MININT32,
+        G_MAXINT32,
+        NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN));
+}
+
+static gint32
+_prop_get_ipv6_temp_valid_lifetime(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    gint32           temp_valid_lifetime;
+    NMConnection    *connection;
+
+    g_return_val_if_fail(self, 0);
+
+    /* 1.) First look at the per-connection setting. If it is not 0 (unknown), use it. */
+    connection = nm_device_get_applied_connection(self);
+    if (connection) {
+        NMSettingIPConfig *s_ip6 = nm_connection_get_setting_ip6_config(connection);
+
+        if (s_ip6) {
+            temp_valid_lifetime =
+                nm_setting_ip6_config_get_temp_valid_lifetime(NM_SETTING_IP6_CONFIG(s_ip6));
+            if (temp_valid_lifetime)
+                return temp_valid_lifetime;
+        }
+    }
+
+    /* 2.) Use the default value from the configuration. */
+    temp_valid_lifetime =
+        nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA,
+                                                    NM_CON_DEFAULT("ipv6.temp-valid-lifetime"),
+                                                    self,
+                                                    0,
+                                                    G_MAXINT32,
+                                                    0);
+    if (temp_valid_lifetime)
+        return temp_valid_lifetime;
+
+    /* 3.) No valid default value configured. Fall back to the original value
+     * from before NM started. */
+    return _nm_utils_ascii_str_to_int64(
+        g_hash_table_lookup(priv->ip6_saved_properties, "temp_valid_lft"),
+        10,
+        0,
+        G_MAXINT32,
+        SECONDS_PER_WEEK /* final hardcoded fallback: 1 week */);
+}
+
+static gint32
+_prop_get_ipv6_temp_preferred_lifetime(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    gint32           temp_preferred_lifetime;
+    NMConnection    *connection;
+
+    g_return_val_if_fail(self, 0);
+
+    /* 1.) First look at the per-connection setting. If it is not 0 (unknown), use it. */
+    connection = nm_device_get_applied_connection(self);
+    if (connection) {
+        NMSettingIPConfig *s_ip6 = nm_connection_get_setting_ip6_config(connection);
+
+        if (s_ip6) {
+            temp_preferred_lifetime =
+                nm_setting_ip6_config_get_temp_preferred_lifetime(NM_SETTING_IP6_CONFIG(s_ip6));
+            if (temp_preferred_lifetime)
+                return temp_preferred_lifetime;
+        }
+    }
+
+    /* 2.) Use the default value from the configuration. */
+    temp_preferred_lifetime =
+        nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA,
+                                                    NM_CON_DEFAULT("ipv6.temp-preferred-lifetime"),
+                                                    self,
+                                                    0,
+                                                    G_MAXINT32,
+                                                    0);
+    if (temp_preferred_lifetime)
+        return temp_preferred_lifetime;
+
+    /* 3.) No valid default value configured. Fall back to the original value
+     * from before NM started. */
+    return _nm_utils_ascii_str_to_int64(
+        g_hash_table_lookup(priv->ip6_saved_properties, "temp_prefered_lft"),
+        10,
+        0,
+        G_MAXINT32,
+        SECONDS_PER_DAY /* final hardcoded fallback: 1 day */);
 }
 
 static NMSettingIP6ConfigAddrGenMode
@@ -2359,11 +2448,16 @@ _prop_get_ipv6_addr_gen_mode(NMDevice *self)
 }
 
 static const char *
-_prop_get_x_cloned_mac_address(NMDevice *self, NMConnection *connection, gboolean is_wifi)
+_prop_get_x_cloned_mac_address(NMDevice     *self,
+                               NMConnection *connection,
+                               gboolean      is_wifi,
+                               gboolean     *out_is_default_special)
 {
     NMSetting  *setting;
     const char *addr = NULL;
 
+    NM_SET_OUT(out_is_default_special, FALSE);
+
     setting = nm_connection_get_setting(connection,
                                         is_wifi ? NM_TYPE_SETTING_WIRELESS : NM_TYPE_SETTING_WIRED);
     if (setting) {
@@ -2394,11 +2488,17 @@ _prop_get_x_cloned_mac_address(NMDevice *self, NMConnection *connection, gboolea
                     NM_SETTING_MAC_RANDOMIZATION_DEFAULT,
                     NM_SETTING_MAC_RANDOMIZATION_ALWAYS,
                     NM_SETTING_MAC_RANDOMIZATION_DEFAULT);
-                if (v == NM_SETTING_MAC_RANDOMIZATION_ALWAYS)
+                if (v == NM_SETTING_MAC_RANDOMIZATION_ALWAYS) {
                     addr = NM_CLONED_MAC_RANDOM;
+                    NM_SET_OUT(out_is_default_special, TRUE);
+                }
             }
-        } else if (NM_CLONED_MAC_IS_SPECIAL(a, is_wifi) || nm_utils_hwaddr_valid(a, ETH_ALEN))
+        } else if (NM_CLONED_MAC_IS_SPECIAL(a, is_wifi)) {
+            addr = a;
+            NM_SET_OUT(out_is_default_special, TRUE);
+        } else if (nm_utils_hwaddr_valid(a, ETH_ALEN)) {
             addr = a;
+        }
     }
 
     return addr;
@@ -3726,7 +3826,7 @@ _dev_ip_state_check(NMDevice *self, int addr_family)
     }
 
     if (priv->ip_data_x[IS_IPv4].state == NM_DEVICE_IP_STATE_PENDING
-        && nm_active_connection_get_master(NM_ACTIVE_CONNECTION(priv->act_request.obj))
+        && nm_active_connection_get_controller(NM_ACTIVE_CONNECTION(priv->act_request.obj))
         && !priv->is_enslaved) {
         /* Don't progress into IP_CHECK or SECONDARIES if we're waiting for the
          * master to enslave us. */
@@ -6676,14 +6776,14 @@ nm_device_master_enslave_slave(NMDevice *self, NMDevice *slave, NMConnection *co
 static void
 detach_port_cb(NMDevice *self, GError *error, gpointer user_data)
 {
-    nm_auto_unref_object NMDevice *slave      = user_data;
-    NMDevicePrivate               *slave_priv = NM_DEVICE_GET_PRIVATE(slave);
+    nm_auto_unref_object NMDevice *slave     = user_data;
+    NMDevicePrivate               *port_priv = NM_DEVICE_GET_PRIVATE(slave);
 
-    nm_assert(slave_priv->port_detach_count > 0);
+    nm_assert(port_priv->port_detach_count > 0);
 
-    if (--slave_priv->port_detach_count == 0) {
-        if (slave_priv->state == NM_DEVICE_STATE_DEACTIVATING) {
-            deactivate_ready(slave, slave_priv->port_detach_reason);
+    if (--port_priv->port_detach_count == 0) {
+        if (port_priv->state == NM_DEVICE_STATE_DEACTIVATING) {
+            deactivate_ready(slave, port_priv->port_detach_reason);
         }
     }
 }
@@ -6708,7 +6808,7 @@ nm_device_master_release_slave(NMDevice           *self,
                                NMDeviceStateReason reason)
 {
     NMDevicePrivate          *priv;
-    NMDevicePrivate          *slave_priv;
+    NMDevicePrivate          *port_priv;
     SlaveInfo                *info;
     gs_unref_object NMDevice *self_free  = NULL;
     gs_unref_object NMDevice *slave_free = NULL;
@@ -6735,10 +6835,10 @@ nm_device_master_release_slave(NMDevice           *self,
     if (!info)
         g_return_if_reached();
 
-    priv       = NM_DEVICE_GET_PRIVATE(self);
-    slave_priv = NM_DEVICE_GET_PRIVATE(slave);
+    priv      = NM_DEVICE_GET_PRIVATE(self);
+    port_priv = NM_DEVICE_GET_PRIVATE(slave);
 
-    g_return_if_fail(self == slave_priv->master);
+    g_return_if_fail(self == port_priv->controller);
     nm_assert(slave == info->slave);
     nm_clear_g_cancellable(&info->cancellable);
 
@@ -6754,8 +6854,8 @@ nm_device_master_release_slave(NMDevice           *self,
                                                      detach_port_cb,
                                                      g_object_ref(slave));
         if (ret == NM_TERNARY_DEFAULT) {
-            slave_priv->port_detach_count++;
-            slave_priv->port_detach_reason = reason;
+            port_priv->port_detach_count++;
+            port_priv->port_detach_reason = reason;
         }
     }
 
@@ -6763,9 +6863,9 @@ nm_device_master_release_slave(NMDevice           *self,
     nm_device_slave_notify_release(slave, reason, release_type);
 
     /* keep both alive until the end of the function.
-     * Transfers ownership from slave_priv->master.  */
-    nm_assert(self == slave_priv->master);
-    self_free = g_steal_pointer(&slave_priv->master);
+     * Transfers ownership from port_priv->controller.  */
+    nm_assert(self == port_priv->controller);
+    self_free = g_steal_pointer(&port_priv->controller);
 
     nm_assert(slave == info->slave);
     slave_free = g_steal_pointer(&info->slave);
@@ -6900,7 +7000,7 @@ carrier_changed(NMDevice *self, gboolean carrier)
     if (priv->ignore_carrier && !carrier)
         return;
 
-    if (nm_device_is_master(self)) {
+    if (nm_device_is_controller(self)) {
         if (carrier) {
             /* If needed, also resume IP configuration that is
              * waiting for carrier. */
@@ -7069,7 +7169,7 @@ device_recheck_slave_status(NMDevice *self, const NMPlatformLink *plink)
         plink_master            = nm_platform_link_get(nm_device_get_platform(self), plink->master);
         plink_master_keep_alive = nmp_object_ref(NMP_OBJECT_UP_CAST(plink_master));
     } else {
-        if (priv->master_ifindex == 0)
+        if (priv->controller_ifindex == 0)
             goto out;
         master       = NULL;
         plink_master = NULL;
@@ -7082,17 +7182,17 @@ device_recheck_slave_status(NMDevice *self, const NMPlatformLink *plink)
         goto out;
     }
 
-    priv->master_ifindex = plink->master;
+    priv->controller_ifindex = plink->master;
 
-    if (priv->master) {
-        if (plink->master > 0 && plink->master == nm_device_get_ifindex(priv->master)) {
+    if (priv->controller) {
+        if (plink->master > 0 && plink->master == nm_device_get_ifindex(priv->controller)) {
             /* call add-slave again. We expect @self already to be added to
              * the master, but this also triggers a recheck-assume. */
-            nm_device_master_add_slave(priv->master, self, FALSE);
+            nm_device_master_add_slave(priv->controller, self, FALSE);
             goto out;
         }
 
-        nm_device_master_release_slave(priv->master,
+        nm_device_master_release_slave(priv->controller,
                                        self,
                                        RELEASE_SLAVE_TYPE_NO_CONFIG,
                                        NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED);
@@ -7133,9 +7233,9 @@ device_ifindex_changed_cb(NMManager *manager, NMDevice *device_changed, NMDevice
 {
     NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
 
-    g_return_if_fail(priv->master_ifindex > 0);
+    g_return_if_fail(priv->controller_ifindex > 0);
 
-    if (priv->master_ifindex != nm_device_get_ifindex(device_changed))
+    if (priv->controller_ifindex != nm_device_get_ifindex(device_changed))
         return;
 
     _LOGD(LOGD_DEVICE,
@@ -8157,7 +8257,7 @@ nm_device_unrealize(NMDevice *self, gboolean remove_resources, GError **error)
     if (nm_clear_g_free(&priv->ip_iface_))
         update_prop_ip_iface(self);
 
-    priv->master_ifindex = 0;
+    priv->controller_ifindex = 0;
 
     _set_mtu(self, 0);
 
@@ -8394,7 +8494,7 @@ static gboolean
 nm_device_master_add_slave(NMDevice *self, NMDevice *slave, gboolean configure)
 {
     NMDevicePrivate *priv;
-    NMDevicePrivate *slave_priv;
+    NMDevicePrivate *port_priv;
     SlaveInfo       *info;
     gboolean         changed = FALSE;
 
@@ -8402,8 +8502,8 @@ nm_device_master_add_slave(NMDevice *self, NMDevice *slave, gboolean configure)
     g_return_val_if_fail(NM_IS_DEVICE(slave), FALSE);
     g_return_val_if_fail(NM_DEVICE_GET_CLASS(self)->attach_port, FALSE);
 
-    priv       = NM_DEVICE_GET_PRIVATE(self);
-    slave_priv = NM_DEVICE_GET_PRIVATE(slave);
+    priv      = NM_DEVICE_GET_PRIVATE(self);
+    port_priv = NM_DEVICE_GET_PRIVATE(slave);
 
     info = find_slave_info(self, slave);
 
@@ -8417,8 +8517,8 @@ nm_device_master_add_slave(NMDevice *self, NMDevice *slave, gboolean configure)
         g_return_val_if_fail(nm_device_get_state(slave) >= NM_DEVICE_STATE_DISCONNECTED, FALSE);
 
     if (!info) {
-        g_return_val_if_fail(!slave_priv->master, FALSE);
-        g_return_val_if_fail(!slave_priv->is_enslaved, FALSE);
+        g_return_val_if_fail(!port_priv->controller, FALSE);
+        g_return_val_if_fail(!port_priv->is_enslaved, FALSE);
 
         info            = g_slice_new0(SlaveInfo);
         info->slave     = g_object_ref(slave);
@@ -8426,25 +8526,25 @@ nm_device_master_add_slave(NMDevice *self, NMDevice *slave, gboolean configure)
         info->watch_id =
             g_signal_connect(slave, NM_DEVICE_STATE_CHANGED, G_CALLBACK(slave_state_changed), self);
         c_list_link_tail(&priv->slaves, &info->lst_slave);
-        slave_priv->master = g_object_ref(self);
+        port_priv->controller = g_object_ref(self);
 
         _active_connection_set_state_flags(self, NM_ACTIVATION_STATE_FLAG_MASTER_HAS_SLAVES);
 
         /* no need to emit
          *
-         *   _notify (slave, PROP_MASTER);
+         *   _notify (slave, PROP_CONTROLLER);
          *
-         * because slave_priv->is_enslaved is not true, thus the value
+         * because port_priv->is_enslaved is not true, thus the value
          * didn't change yet. */
 
-        g_warn_if_fail(!NM_FLAGS_HAS(slave_priv->unmanaged_mask, NM_UNMANAGED_IS_SLAVE));
+        g_warn_if_fail(!NM_FLAGS_HAS(port_priv->unmanaged_mask, NM_UNMANAGED_IS_SLAVE));
         nm_device_set_unmanaged_by_flags(slave,
                                          NM_UNMANAGED_IS_SLAVE,
                                          NM_UNMAN_FLAG_OP_SET_MANAGED,
                                          NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED);
         changed = TRUE;
     } else
-        g_return_val_if_fail(slave_priv->master == self, FALSE);
+        g_return_val_if_fail(port_priv->controller == self, FALSE);
 
     nm_device_queue_recheck_assume(self);
     nm_device_queue_recheck_assume(slave);
@@ -8525,38 +8625,40 @@ nm_device_master_release_slaves_all(NMDevice *self)
 }
 
 /**
- * nm_device_is_master:
+ * nm_device_is_controller:
  * @self: the device
  *
- * Returns: %TRUE if the device can have slaves
+ * Returns: %TRUE if the device can have ports
  */
 gboolean
-nm_device_is_master(NMDevice *self)
+nm_device_is_controller(NMDevice *self)
 {
     g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
 
-    return NM_DEVICE_GET_CLASS(self)->is_master;
+    return NM_DEVICE_GET_CLASS(self)->is_controller;
 }
 
 /**
- * nm_device_get_master:
+ * nm_device_get_controller:
  * @self: the device
  *
- * If @self has been enslaved by another device, this returns that
+ * If @self has been set as port by another device, this returns that
  * device. Otherwise, it returns %NULL. (In particular, note that if
- * @self is in the process of activating as a slave, but has not yet
- * been enslaved by its master, this will return %NULL.)
+ * @self is in the process of activating as a port, but has not yet
+ * been set as port by its controller, this will return %NULL.)
+ *
+ * Returns: (transfer none): @self's controller, or %NULL
  *
- * Returns: (transfer none): @self's master, or %NULL
+ * Since: 1.48
  */
 NMDevice *
-nm_device_get_master(NMDevice *self)
+nm_device_get_controller(NMDevice *self)
 {
     NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
 
     if (priv->is_enslaved) {
-        g_return_val_if_fail(priv->master, NULL);
-        return priv->master;
+        g_return_val_if_fail(priv->controller, NULL);
+        return priv->controller;
     }
     return NULL;
 }
@@ -8576,7 +8678,7 @@ nm_device_slave_notify_enslave(NMDevice *self, gboolean success)
     NMConnection    *connection = nm_device_get_applied_connection(self);
     gboolean         activating = (priv->state == NM_DEVICE_STATE_IP_CONFIG);
 
-    g_return_if_fail(priv->master);
+    g_return_if_fail(priv->controller);
 
     if (!priv->is_enslaved) {
         if (success) {
@@ -8585,14 +8687,15 @@ nm_device_slave_notify_enslave(NMDevice *self, gboolean success)
                       "Activation: connection '%s' enslaved, continuing activation",
                       nm_connection_get_id(connection));
             } else
-                _LOGI(LOGD_DEVICE, "enslaved to %s", nm_device_get_iface(priv->master));
+                _LOGI(LOGD_DEVICE, "enslaved to %s", nm_device_get_iface(priv->controller));
 
             priv->is_enslaved = TRUE;
 
-            _notify(self, PROP_MASTER);
+            _notify(priv->controller, PROP_CONTROLLER);
 
-            nm_clear_pointer(&NM_DEVICE_GET_PRIVATE(priv->master)->ports_variant, g_variant_unref);
-            nm_gobject_notify_together(priv->master, PROP_PORTS, PROP_SLAVES);
+            nm_clear_pointer(&NM_DEVICE_GET_PRIVATE(priv->controller)->ports_variant,
+                             g_variant_unref);
+            nm_gobject_notify_together(priv->controller, PROP_PORTS, PROP_SLAVES);
         } else if (activating) {
             _LOGW(LOGD_DEVICE,
                   "Activation: connection '%s' could not be enslaved",
@@ -8629,7 +8732,7 @@ nm_device_slave_notify_release(NMDevice           *self,
     NMConnection    *connection = nm_device_get_applied_connection(self);
     const char      *master_status;
 
-    g_return_if_fail(priv->master);
+    g_return_if_fail(priv->controller);
 
     if (!priv->is_enslaved && release_type == RELEASE_SLAVE_TYPE_NO_CONFIG)
         return;
@@ -8661,14 +8764,14 @@ nm_device_slave_notify_release(NMDevice           *self,
         _cancel_activation(self);
         nm_device_queue_state(self, NM_DEVICE_STATE_DEACTIVATING, reason);
     } else
-        _LOGI(LOGD_DEVICE, "released from master device %s", nm_device_get_iface(priv->master));
+        _LOGI(LOGD_DEVICE, "released from master device %s", nm_device_get_iface(priv->controller));
 
     priv->is_enslaved = FALSE;
 
-    _notify(self, PROP_MASTER);
+    _notify(priv->controller, PROP_CONTROLLER);
 
-    nm_clear_pointer(&NM_DEVICE_GET_PRIVATE(priv->master)->ports_variant, g_variant_unref);
-    nm_gobject_notify_together(priv->master, PROP_PORTS, PROP_SLAVES);
+    nm_clear_pointer(&NM_DEVICE_GET_PRIVATE(priv->controller)->ports_variant, g_variant_unref);
+    nm_gobject_notify_together(priv->controller, PROP_PORTS, PROP_SLAVES);
 }
 
 /**
@@ -8692,10 +8795,10 @@ nm_device_removed(NMDevice *self, gboolean unconfigure_ip_config)
     _dev_ipdhcpx_cleanup(self, AF_INET6, TRUE, FALSE);
 
     priv = NM_DEVICE_GET_PRIVATE(self);
-    if (priv->master) {
+    if (priv->controller) {
         /* this is called when something externally messes with the slave or during shut-down.
          * Release the slave from master, but don't touch the device. */
-        nm_device_master_release_slave(priv->master,
+        nm_device_master_release_slave(priv->controller,
                                        self,
                                        RELEASE_SLAVE_TYPE_NO_CONFIG,
                                        NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED);
@@ -8728,7 +8831,7 @@ is_available(NMDevice *self, NMDeviceCheckDevAvailableFlags flags)
         return TRUE;
 
     /* master types are always available even without carrier. */
-    if (nm_device_is_master(self))
+    if (nm_device_is_controller(self))
         return TRUE;
 
     return FALSE;
@@ -8768,7 +8871,7 @@ gboolean
 nm_device_ignore_carrier_by_default(NMDevice *self)
 {
     /* master types ignore-carrier by default. */
-    return nm_device_is_master(self);
+    return nm_device_is_controller(self);
 }
 
 gboolean
@@ -9207,7 +9310,7 @@ nm_device_generate_connection(NMDevice *self,
         && NM_IN_STRSET(ip6_method,
                         NM_SETTING_IP6_CONFIG_METHOD_IGNORE,
                         NM_SETTING_IP6_CONFIG_METHOD_DISABLED)
-        && !nm_setting_connection_get_master(NM_SETTING_CONNECTION(s_con))
+        && !nm_setting_connection_get_controller(NM_SETTING_CONNECTION(s_con))
         && c_list_is_empty(&priv->slaves)) {
         NM_SET_OUT(out_maybe_later, TRUE);
         g_set_error_literal(
@@ -9223,7 +9326,7 @@ nm_device_generate_connection(NMDevice *self,
      */
     if (nm_streq0(ip4_method, NM_SETTING_IP4_CONFIG_METHOD_DISABLED)
         && nm_streq0(ip6_method, NM_SETTING_IP6_CONFIG_METHOD_LINK_LOCAL)
-        && !nm_setting_connection_get_master(NM_SETTING_CONNECTION(s_con))
+        && !nm_setting_connection_get_controller(NM_SETTING_CONNECTION(s_con))
         && c_list_is_empty(&priv->slaves)
         && !nm_config_data_get_assume_ipv6ll_only(NM_CONFIG_GET_DATA, self)) {
         _LOGD(LOGD_DEVICE,
@@ -9492,7 +9595,7 @@ nm_device_check_slave_connection_compatible(NMDevice *self, NMConnection *slave)
     g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
     g_return_val_if_fail(NM_IS_CONNECTION(slave), FALSE);
 
-    if (!nm_device_is_master(self))
+    if (!nm_device_is_controller(self))
         return FALSE;
 
     /* All masters should have connection type set */
@@ -9508,6 +9611,17 @@ nm_device_check_slave_connection_compatible(NMDevice *self, NMConnection *slave)
     return nm_streq(connection_type, slave_type);
 }
 
+gboolean
+nm_device_can_be_parent(NMDevice *self)
+{
+    NMDeviceType device_type = nm_device_get_device_type(self);
+
+    if ((device_type == NM_DEVICE_TYPE_OVS_BRIDGE) || (device_type == NM_DEVICE_TYPE_OVS_PORT))
+        return FALSE;
+    else
+        return TRUE;
+}
+
 /**
  * nm_device_can_assume_connections:
  * @self: #NMDevice instance
@@ -9790,17 +9904,17 @@ master_ready(NMDevice *self, NMActiveConnection *active)
     NMActiveConnection *master_connection;
     NMDevice           *master;
 
-    /* Notify a master device that it has a new slave */
-    nm_assert(nm_active_connection_get_master_ready(active));
+    /* Notify a controller device that it has a new port */
+    nm_assert(nm_active_connection_get_controller_ready(active));
 
-    master_connection = nm_active_connection_get_master(active);
+    master_connection = nm_active_connection_get_controller(active);
 
     master = nm_active_connection_get_device(master_connection);
 
     _LOGD(LOGD_DEVICE, "master connection ready; master device %s", nm_device_get_iface(master));
 
-    if (priv->master && priv->master != master)
-        nm_device_master_release_slave(priv->master,
+    if (priv->controller && priv->controller != master)
+        nm_device_master_release_slave(priv->controller,
                                        self,
                                        RELEASE_SLAVE_TYPE_NO_CONFIG,
                                        NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED);
@@ -9816,7 +9930,7 @@ master_ready_cb(NMActiveConnection *active, GParamSpec *pspec, NMDevice *self)
 {
     NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
 
-    nm_assert(nm_active_connection_get_master_ready(active));
+    nm_assert(nm_active_connection_get_controller_ready(active));
 
     if (priv->state == NM_DEVICE_STATE_PREPARE)
         nm_device_activate_schedule_stage1_device_prepare(self, FALSE);
@@ -10063,7 +10177,7 @@ activate_stage1_device_prepare(NMDevice *self)
     }
 
     active = NM_ACTIVE_CONNECTION(priv->act_request.obj);
-    master = nm_active_connection_get_master(active);
+    master = nm_active_connection_get_controller(active);
     if (master) {
         if (nm_active_connection_get_state(master) >= NM_ACTIVE_CONNECTION_STATE_DEACTIVATING) {
             NMDevice           *master_device  = nm_active_connection_get_device(master);
@@ -10078,24 +10192,24 @@ activate_stage1_device_prepare(NMDevice *self)
             nm_device_state_changed(self, NM_DEVICE_STATE_FAILED, failure_reason);
             return;
         }
-        /* If the master connection is ready for slaves, attach ourselves */
-        if (!nm_active_connection_get_master_ready(active)) {
-            if (priv->master_ready_id == 0) {
-                _LOGD(LOGD_DEVICE, "waiting for master connection to become ready");
-                priv->master_ready_id =
+        /* If the controller connection is ready for ports, attach ourselves */
+        if (!nm_active_connection_get_controller_ready(active)) {
+            if (priv->controller_ready_id == 0) {
+                _LOGD(LOGD_DEVICE, "waiting for controller connection to become ready");
+                priv->controller_ready_id =
                     g_signal_connect(active,
-                                     "notify::" NM_ACTIVE_CONNECTION_INT_MASTER_READY,
+                                     "notify::" NM_ACTIVE_CONNECTION_INT_CONTROLLER_READY,
                                      G_CALLBACK(master_ready_cb),
                                      self);
             }
             return;
         }
     }
-    nm_clear_g_signal_handler(priv->act_request.obj, &priv->master_ready_id);
+    nm_clear_g_signal_handler(priv->act_request.obj, &priv->controller_ready_id);
     if (master)
         master_ready(self, active);
-    else if (priv->master) {
-        nm_device_master_release_slave(priv->master,
+    else if (priv->controller) {
+        nm_device_master_release_slave(priv->controller,
                                        self,
                                        RELEASE_SLAVE_TYPE_CONFIG_FORCE,
                                        NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED);
@@ -10804,13 +10918,40 @@ _dev_ipdhcpx_set_state(NMDevice *self, int addr_family, NMDeviceIPState state)
 }
 
 static void
-_dev_ipdhcpx_cleanup(NMDevice *self, int addr_family, gboolean full_cleanup, gboolean release)
+_dev_ipdhcpx_cleanup(NMDevice *self, int addr_family, gboolean full_cleanup, gboolean force_release)
 {
-    NMDevicePrivate *priv    = NM_DEVICE_GET_PRIVATE(self);
-    const int        IS_IPv4 = NM_IS_IPv4(addr_family);
+    NMDevicePrivate   *priv    = NM_DEVICE_GET_PRIVATE(self);
+    const int          IS_IPv4 = NM_IS_IPv4(addr_family);
+    NMSettingIPConfig *s_ip4   = NULL;
+    NMSettingIPConfig *s_ip6   = NULL;
+    gboolean           release;
 
     _dev_ipdhcpx_set_state(self, addr_family, NM_DEVICE_IP_STATE_NONE);
 
+    if (IS_IPv4)
+        s_ip4 = nm_device_get_applied_setting(self, NM_TYPE_SETTING_IP4_CONFIG);
+    else
+        s_ip6 = nm_device_get_applied_setting(self, NM_TYPE_SETTING_IP6_CONFIG);
+
+    if ((IS_IPv4 && s_ip4) || (!IS_IPv4 && s_ip6)) {
+        if (nm_setting_ip_config_get_dhcp_send_release(IS_IPv4 ? s_ip4 : s_ip6)
+            == NM_TERNARY_DEFAULT)
+            release = nm_config_data_get_connection_default_int64(
+                NM_CONFIG_GET_DATA,
+                IS_IPv4 ? NM_CON_DEFAULT("ipv4.dhcp-send-release")
+                        : NM_CON_DEFAULT("ipv6.dhcp-send-release"),
+                self,
+                NM_TERNARY_FALSE,
+                NM_TERNARY_TRUE,
+                NM_TERNARY_FALSE);
+        else
+            release = nm_setting_ip_config_get_dhcp_send_release(IS_IPv4 ? s_ip4 : s_ip6);
+
+        release = force_release || (release && full_cleanup);
+    } else {
+        release = force_release;
+    }
+
     if (full_cleanup && !IS_IPv4) {
         priv->ipdhcp_data_6.v6.mode            = NM_NDISC_DHCP_LEVEL_NONE;
         priv->ipdhcp_data_6.v6.needed_prefixes = 0;
@@ -11306,7 +11447,7 @@ connection_requires_carrier(NMConnection *connection)
     /* We can progress to IP_CONFIG now, so that we're enslaved.
      * That may actually cause carrier to go up and thus continue activation. */
     s_con = nm_connection_get_setting_connection(connection);
-    if (nm_setting_connection_get_master(s_con))
+    if (nm_setting_connection_get_controller(s_con))
         return FALSE;
 
     ip4_carrier_wanted = connection_ip_method_requires_carrier(connection, AF_INET, &ip4_used);
@@ -11373,25 +11514,32 @@ nm_device_needs_ip6_subnet(NMDevice *self)
 void
 nm_device_use_ip6_subnet(NMDevice *self, const NMPlatformIP6Address *subnet)
 {
-    nm_auto_unref_l3cd_init NML3ConfigData *l3cd = NULL;
-    char                                    sbuf[NM_UTILS_TO_STRING_BUFFER_SIZE];
-    NMPlatformIP6Address                    address;
+    NMConnection *connection = nm_device_get_applied_connection(self);
 
-    l3cd = nm_device_create_l3_config_data(self, NM_IP_CONFIG_SOURCE_SHARED);
+    if (connection) {
+        NMSettingIPConfig *s_ip6 = nm_connection_get_setting_ip6_config(connection);
 
-    /* Assign a ::1 address in the subnet for us. */
-    address = *subnet;
-    address.address.s6_addr32[3] |= htonl(1);
+        if (nm_streq(nm_setting_ip_config_get_method(s_ip6), NM_SETTING_IP6_CONFIG_METHOD_SHARED)) {
+            nm_auto_unref_l3cd_init NML3ConfigData *l3cd = NULL;
+            char                                    sbuf[NM_UTILS_TO_STRING_BUFFER_SIZE];
+            NMPlatformIP6Address                    address;
+            l3cd = nm_device_create_l3_config_data(self, NM_IP_CONFIG_SOURCE_SHARED);
 
-    nm_l3_config_data_add_address_6(l3cd, &address);
+            /* Assign a ::1 address in the subnet for us. */
+            address = *subnet;
+            address.address.s6_addr32[3] |= htonl(1);
 
-    _LOGD(LOGD_IP6,
-          "ipv6-pd: using %s",
-          nm_platform_ip6_address_to_string(&address, sbuf, sizeof(sbuf)));
+            nm_l3_config_data_add_address_6(l3cd, &address);
 
-    _dev_l3_register_l3cds_set_one(self, L3_CONFIG_DATA_TYPE_PD_6, l3cd, FALSE);
-    _dev_l3_cfg_commit(self, TRUE);
-    _dev_ipac6_ndisc_set_router_config(self);
+            _LOGD(LOGD_IP6,
+                  "ipv6-pd: using %s",
+                  nm_platform_ip6_address_to_string(&address, sbuf, sizeof(sbuf)));
+
+            _dev_l3_register_l3cds_set_one(self, L3_CONFIG_DATA_TYPE_PD_6, l3cd, FALSE);
+            _dev_l3_cfg_commit(self, TRUE);
+            _dev_ipac6_ndisc_set_router_config(self);
+        }
+    }
 }
 
 /*
@@ -11768,12 +11916,12 @@ _set_mtu(NMDevice *self, guint32 mtu)
     priv->mtu = mtu;
     _notify(self, PROP_MTU);
 
-    if (priv->master) {
+    if (priv->controller) {
         /* changing the MTU of a slave, might require the master to reset
          * its MTU. Note that the master usually cannot set a MTU larger
          * then the slave's. Hence, when the slave increases the MTU,
          * master might want to retry setting the MTU. */
-        nm_device_commit_mtu(priv->master);
+        nm_device_commit_mtu(priv->controller);
     }
 }
 
@@ -12377,6 +12525,8 @@ _dev_sysctl_save_ip6_properties(NMDevice *self)
         "disable_ipv6",
         "hop_limit",
         "use_tempaddr",
+        "temp_valid_lft",
+        "temp_prefered_lft",
     };
     NMDevicePrivate *priv     = NM_DEVICE_GET_PRIVATE(self);
     NMPlatform      *platform = nm_device_get_platform(self);
@@ -12476,6 +12626,17 @@ _dev_addrgenmode6_set(NMDevice *self, guint8 addr_gen_mode)
         }
     }
 
+    nm_device_sysctl_ip_conf_set(
+        self,
+        AF_INET6,
+        "temp_valid_lft",
+        nm_sprintf_buf(sbuf, "%u", (unsigned) _prop_get_ipv6_temp_valid_lifetime(self)));
+    nm_device_sysctl_ip_conf_set(
+        self,
+        AF_INET6,
+        "temp_prefered_lft",
+        nm_sprintf_buf(sbuf, "%u", (unsigned) _prop_get_ipv6_temp_preferred_lifetime(self)));
+
     if (addr_gen_mode == NM_IN6_ADDR_GEN_MODE_NONE) {
         gs_free char *value = NULL;
 
@@ -12536,7 +12697,7 @@ activate_stage3_ip_config_for_addr_family(NMDevice *self, int addr_family, const
     ip_ifindex = nm_device_get_ip_ifindex(self);
 
     if (connection_ip_method_requires_carrier(connection, addr_family, NULL)
-        && nm_device_is_master(self) && !priv->carrier) {
+        && nm_device_is_controller(self) && !priv->carrier) {
         if (!priv->ip_data_x[IS_IPv4].wait_for_carrier) {
             _LOGT_ip(addr_family, "waiting until carrier is on");
             priv->ip_data_x[IS_IPv4].wait_for_carrier = TRUE;
@@ -12548,7 +12709,7 @@ activate_stage3_ip_config_for_addr_family(NMDevice *self, int addr_family, const
         priv->ip_data_x[IS_IPv4].wait_for_carrier = FALSE;
     }
 
-    if (nm_device_is_master(self) && ip_requires_slaves(self, addr_family)) {
+    if (nm_device_is_controller(self) && ip_requires_slaves(self, addr_family)) {
         /* If the master has no ready slaves, and depends on slaves for
          * a successful IP configuration attempt, then postpone IP addressing.
          */
@@ -12595,7 +12756,7 @@ activate_stage3_ip_config_for_addr_family(NMDevice *self, int addr_family, const
         } else if (nm_streq(method, NM_SETTING_IP6_CONFIG_METHOD_IGNORE)) {
             if (!priv->ip_data_x[IS_IPv4].is_ignore) {
                 priv->ip_data_x[IS_IPv4].is_ignore = TRUE;
-                if (priv->master) {
+                if (priv->controller) {
                     /* If a device only has an IPv6 link-local address,
                      * we don't generate an assumed connection. Therefore,
                      * when a new slave connection (without IP configuration)
@@ -14959,8 +15120,39 @@ _unmanaged_flags2str(NMUnmanagedFlags flags, NMUnmanagedFlags mask, char *buf, g
     return buf;
 }
 
+static NMDeviceStateReason
+unmanaged_flags_to_reason(NMUnmanagedFlags flags)
+{
+    /* Even if there are multiple flags, we can only return one reason.
+     * Return the most important reason.
+     */
+    if (NM_FLAGS_HAS(flags, NM_UNMANAGED_SLEEPING))
+        return NM_DEVICE_STATE_REASON_UNMANAGED_SLEEPING;
+    if (NM_FLAGS_HAS(flags, NM_UNMANAGED_QUITTING))
+        return NM_DEVICE_STATE_REASON_UNMANAGED_QUITTING;
+    if (NM_FLAGS_HAS(flags, NM_UNMANAGED_USER_SETTINGS))
+        return NM_DEVICE_STATE_REASON_UNMANAGED_USER_SETTINGS;
+    if (NM_FLAGS_HAS(flags, NM_UNMANAGED_PLATFORM_INIT))
+        return NM_DEVICE_STATE_REASON_UNMANAGED_LINK_NOT_INIT;
+    if (NM_FLAGS_HAS(flags, NM_UNMANAGED_USER_UDEV))
+        return NM_DEVICE_STATE_REASON_UNMANAGED_USER_UDEV;
+    if (NM_FLAGS_HAS(flags, NM_UNMANAGED_USER_EXPLICIT))
+        return NM_DEVICE_STATE_REASON_UNMANAGED_USER_EXPLICIT;
+    if (NM_FLAGS_HAS(flags, NM_UNMANAGED_USER_CONF))
+        return NM_DEVICE_STATE_REASON_UNMANAGED_USER_CONF;
+    if (NM_FLAGS_HAS(flags, NM_UNMANAGED_BY_DEFAULT))
+        return NM_DEVICE_STATE_REASON_UNMANAGED_BY_DEFAULT;
+    if (NM_FLAGS_HAS(flags, NM_UNMANAGED_EXTERNAL_DOWN))
+        return NM_DEVICE_STATE_REASON_UNMANAGED_EXTERNAL_DOWN;
+
+    return NM_DEVICE_STATE_REASON_NOW_UNMANAGED;
+}
+
 static gboolean
-_get_managed_by_flags(NMUnmanagedFlags flags, NMUnmanagedFlags mask, gboolean for_user_request)
+_get_managed_by_flags(NMUnmanagedFlags     flags,
+                      NMUnmanagedFlags     mask,
+                      gboolean             for_user_request,
+                      NMDeviceStateReason *unmanaged_reason)
 {
     /* Evaluate the managed state based on the unmanaged flags.
      *
@@ -14979,7 +15171,7 @@ _get_managed_by_flags(NMUnmanagedFlags flags, NMUnmanagedFlags mask, gboolean fo
          *
          * Effectively, this check is redundant, as the code below already
          * already ensures that. Still, express this invariant explicitly here. */
-        if (_get_managed_by_flags(flags, mask, FALSE))
+        if (_get_managed_by_flags(flags, mask, FALSE, unmanaged_reason))
             return TRUE;
 
         /* A for-user-request, is effectively the same as pretending
@@ -15022,7 +15214,12 @@ _get_managed_by_flags(NMUnmanagedFlags flags, NMUnmanagedFlags mask, gboolean fo
                    | NM_UNMANAGED_EXTERNAL_DOWN);
     }
 
-    return flags == NM_UNMANAGED_NONE;
+    if (flags == NM_UNMANAGED_NONE) {
+        return TRUE;
+    } else {
+        NM_SET_OUT(unmanaged_reason, unmanaged_flags_to_reason(flags));
+        return FALSE;
+    }
 }
 
 /**
@@ -15051,7 +15248,10 @@ nm_device_get_managed(NMDevice *self, gboolean for_user_request)
 
     priv = NM_DEVICE_GET_PRIVATE(self);
 
-    return _get_managed_by_flags(priv->unmanaged_flags, priv->unmanaged_mask, for_user_request);
+    return _get_managed_by_flags(priv->unmanaged_flags,
+                                 priv->unmanaged_mask,
+                                 for_user_request,
+                                 NULL);
 }
 
 /**
@@ -15190,9 +15390,9 @@ _set_unmanaged_flags(NMDevice           *self,
           (priv->unmanaged_flags | priv->unmanaged_mask) ? "=" : "",
           (guint) priv->unmanaged_flags,
           (guint) priv->unmanaged_mask,
-          (_get_managed_by_flags(priv->unmanaged_flags, priv->unmanaged_mask, FALSE)
+          (_get_managed_by_flags(priv->unmanaged_flags, priv->unmanaged_mask, FALSE, NULL)
                ? "managed"
-               : (_get_managed_by_flags(priv->unmanaged_flags, priv->unmanaged_mask, TRUE)
+               : (_get_managed_by_flags(priv->unmanaged_flags, priv->unmanaged_mask, TRUE, NULL)
                       ? "manageable"
                       : "unmanaged")),
           priv->real ? "" : "/unrealized",
@@ -15211,6 +15411,9 @@ _set_unmanaged_flags(NMDevice           *self,
     if (transition_state) {
         new_state = was_managed ? NM_DEVICE_STATE_UNMANAGED : NM_DEVICE_STATE_UNAVAILABLE;
         if (new_state == NM_DEVICE_STATE_UNMANAGED) {
+            /* In state UNMANAGED, the reason always depends on current flags, not on what
+             * the caller passed. */
+            _get_managed_by_flags(priv->unmanaged_flags, priv->unmanaged_mask, FALSE, &reason);
             _cancel_activation(self);
         } else {
             /* The assume check should happen before the device transitions to
@@ -15225,6 +15428,13 @@ _set_unmanaged_flags(NMDevice           *self,
             nm_device_state_changed(self, new_state, reason);
         else
             nm_device_queue_state(self, new_state, reason);
+    } else {
+        /* No state change, but possibly update the reason in UNMANAGED */
+        if (!_get_managed_by_flags(priv->unmanaged_flags, priv->unmanaged_mask, FALSE, &reason)
+            && reason != priv->state_reason) {
+            priv->state_reason = reason;
+            _notify(self, PROP_STATE_REASON);
+        }
     }
 }
 
@@ -15483,7 +15693,7 @@ nm_device_update_firewall_zone(NMDevice *self)
 void
 nm_device_update_metered(NMDevice *self)
 {
-#define NM_METERED_INVALID ((NMMetered) -1)
+#define NM_METERED_INVALID ((NMMetered) - 1)
     NMDevicePrivate     *priv = NM_DEVICE_GET_PRIVATE(self);
     NMSettingConnection *setting;
     NMMetered            conn_value, value = NM_METERED_INVALID;
@@ -15773,7 +15983,7 @@ check_connection_available(NMDevice                      *self,
         return TRUE;
     }
 
-    if (nm_device_is_master(self)) {
+    if (nm_device_is_controller(self)) {
         /* master types are always available even without carrier.
          * Making connection non-available would un-enslave slaves which
          * is not desired. */
@@ -16164,12 +16374,12 @@ _cleanup_generic_post(NMDevice *self, NMDeviceStateReason reason, CleanupType cl
         nm_active_connection_set_default(NM_ACTIVE_CONNECTION(priv->act_request.obj),
                                          AF_INET,
                                          FALSE);
-        nm_clear_g_signal_handler(priv->act_request.obj, &priv->master_ready_id);
+        nm_clear_g_signal_handler(priv->act_request.obj, &priv->controller_ready_id);
         act_request_set(self, NULL);
     }
 
     if (cleanup_type == CLEANUP_TYPE_DECONFIGURE
-        && ((reason == NM_DEVICE_STATE_REASON_CARRIER && nm_device_is_master(self))
+        && ((reason == NM_DEVICE_STATE_REASON_CARRIER && nm_device_is_controller(self))
             || !NM_IN_SET(reason,
                           NM_DEVICE_STATE_REASON_NOW_MANAGED,
                           NM_DEVICE_STATE_REASON_CARRIER))) {
@@ -16253,9 +16463,9 @@ nm_device_cleanup(NMDevice *self, NMDeviceStateReason reason, CleanupType cleanu
         nm_platform_ip4_dev_route_blacklist_set(nm_device_get_platform(self), ifindex, NULL);
 
     /* slave: mark no longer enslaved */
-    if (priv->master && priv->ifindex > 0
+    if (priv->controller && priv->ifindex > 0
         && nm_platform_link_get_master(nm_device_get_platform(self), priv->ifindex) <= 0) {
-        nm_device_master_release_slave(priv->master,
+        nm_device_master_release_slave(priv->controller,
                                        self,
                                        RELEASE_SLAVE_TYPE_NO_CONFIG,
                                        NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED);
@@ -16666,6 +16876,10 @@ _set_state_full(NMDevice *self, NMDeviceState state, NMDeviceStateReason reason,
             _cleanup_ip_pre(self, AF_INET6, CLEANUP_TYPE_DECONFIGURE, FALSE);
         }
         break;
+    case NM_DEVICE_STATE_DEACTIVATING:
+        /* If we are now deactivating we should enforce IP cleanup. */
+        _cleanup_ip_pre(self, AF_INET, CLEANUP_TYPE_DECONFIGURE, FALSE);
+        _cleanup_ip_pre(self, AF_INET6, CLEANUP_TYPE_DECONFIGURE, FALSE);
     default:
         break;
     }
@@ -17431,6 +17645,8 @@ nm_device_hw_addr_set(NMDevice *self, const char *addr, const char *detail, gboo
  * @hwaddr: (out): the cloned MAC address to set on interface
  * @hwaddr_type: (out): the type of address to set
  * @hwaddr_detail: (out): the detail (origin) of address to set
+ * @is_default_special (out): if %TRUE, the cloned MAC comes from
+ *   global default configuration and is a special keyword
  * @error: on return, an error or %NULL
  *
  * Computes the MAC to be set on a interface. On success, one of the
@@ -17451,6 +17667,7 @@ _hw_addr_get_cloned(NMDevice     *self,
                     char        **hwaddr,
                     HwAddrType   *hwaddr_type,
                     const char  **hwaddr_detail,
+                    gboolean     *is_default_special,
                     GError      **error)
 {
     NMDevicePrivate *priv;
@@ -17469,7 +17686,7 @@ _hw_addr_get_cloned(NMDevice     *self,
     if (!connection)
         g_return_val_if_reached(FALSE);
 
-    addr_setting = _prop_get_x_cloned_mac_address(self, connection, is_wifi);
+    addr_setting = _prop_get_x_cloned_mac_address(self, connection, is_wifi, is_default_special);
 
     addr = addr_setting;
 
@@ -17605,7 +17822,7 @@ nm_device_hw_addr_get_cloned(NMDevice     *self,
                              gboolean     *preserve,
                              GError      **error)
 {
-    if (!_hw_addr_get_cloned(self, connection, is_wifi, preserve, hwaddr, NULL, NULL, error))
+    if (!_hw_addr_get_cloned(self, connection, is_wifi, preserve, hwaddr, NULL, NULL, NULL, error))
         return FALSE;
 
     return TRUE;
@@ -17615,11 +17832,13 @@ gboolean
 nm_device_hw_addr_set_cloned(NMDevice *self, NMConnection *connection, gboolean is_wifi)
 {
     NMDevicePrivate      *priv;
-    gboolean              preserve = FALSE;
-    gs_free char         *hwaddr   = NULL;
-    const char           *detail   = NULL;
-    HwAddrType            type     = HW_ADDR_TYPE_UNSET;
-    gs_free_error GError *error    = NULL;
+    gboolean              preserve           = FALSE;
+    gs_free char         *hwaddr             = NULL;
+    const char           *detail             = NULL;
+    HwAddrType            type               = HW_ADDR_TYPE_UNSET;
+    gs_free_error GError *error              = NULL;
+    gboolean              is_default_special = FALSE;
+    gboolean              ret;
 
     g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
     priv = NM_DEVICE_GET_PRIVATE(self);
@@ -17631,6 +17850,7 @@ nm_device_hw_addr_set_cloned(NMDevice *self, NMConnection *connection, gboolean
                              &hwaddr,
                              &type,
                              &detail,
+                             &is_default_special,
                              &error)) {
         _LOGW(LOGD_DEVICE, "set-hw-addr: %s", error->message);
         return FALSE;
@@ -17640,8 +17860,22 @@ nm_device_hw_addr_set_cloned(NMDevice *self, NMConnection *connection, gboolean
         return nm_device_hw_addr_reset(self, detail);
 
     if (hwaddr) {
+        ret = _hw_addr_set(self, hwaddr, "set-cloned", detail);
+        if (!ret && is_default_special) {
+            /* If the distro sets a global special value for the cloned MAC (for
+             * example, "stable-ssid") and the driver doesn't support changing the
+             * MAC, all activations will fail on the interface unless users know
+             * that they need to change the cloned MAC. Be more tolerant to errors
+             * in case the MAC is global and special.
+             */
+            _LOGE(LOGD_DEVICE,
+                  "ignore error changing the MAC address to globally configured value \"%s\","
+                  "the device does not support it",
+                  detail);
+            return TRUE;
+        }
         priv->hw_addr_type = type;
-        return _hw_addr_set(self, hwaddr, "set-cloned", detail);
+        return ret;
     }
 
     return TRUE;
@@ -18283,8 +18517,8 @@ get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
     case PROP_PHYSICAL_PORT_ID:
         g_value_set_string(value, priv->physical_port_id);
         break;
-    case PROP_MASTER:
-        g_value_set_object(value, nm_device_get_master(self));
+    case PROP_CONTROLLER:
+        g_value_set_object(value, nm_device_get_controller(self));
         break;
     case PROP_PARENT:
         g_value_set_string(value, nm_dbus_track_obj_path_get(&priv->parent_device));
@@ -18602,7 +18836,7 @@ dispose(GObject *object)
 
     _cleanup_generic_post(self, NM_DEVICE_STATE_REASON_NONE, CLEANUP_TYPE_KEEP);
 
-    nm_assert(priv->master_ready_id == 0);
+    nm_assert(priv->controller_ready_id == 0);
 
     g_hash_table_remove_all(priv->ip6_saved_properties);
 
@@ -19052,11 +19286,12 @@ nm_device_class_init(NMDeviceClass *klass)
                             "",
                             NULL,
                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
-    obj_properties[PROP_MASTER] = g_param_spec_object(NM_DEVICE_MASTER,
-                                                      "",
-                                                      "",
-                                                      NM_TYPE_DEVICE,
-                                                      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_CONTROLLER] =
+        g_param_spec_object(NM_DEVICE_CONTROLLER,
+                            "",
+                            "",
+                            NM_TYPE_DEVICE,
+                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
     obj_properties[PROP_PARENT] = g_param_spec_string(NM_DEVICE_PARENT,
                                                       "",
                                                       "",
diff --git a/src/core/devices/nm-device.h b/src/core/devices/nm-device.h
index b096d23a..ffe6b1af 100644
--- a/src/core/devices/nm-device.h
+++ b/src/core/devices/nm-device.h
@@ -66,7 +66,7 @@
 
 #define NM_DEVICE_TYPE_DESC          "type-desc"          /* Internal only */
 #define NM_DEVICE_IFINDEX            "ifindex"            /* Internal only */
-#define NM_DEVICE_MASTER             "master"             /* Internal only */
+#define NM_DEVICE_CONTROLLER         "controller"         /* Internal only */
 #define NM_DEVICE_HAS_PENDING_ACTION "has-pending-action" /* Internal only */
 
 /* Internal signals */
@@ -186,9 +186,9 @@ typedef struct _NMDeviceClass {
      * a delta in the MTU allowed value due the encapsulation overhead */
     guint16 mtu_parent_delta;
 
-    /* Whether the device type is a master-type. This depends purely on the
+    /* Whether the device type is a controller-type. This depends purely on the
      * type (NMDeviceClass), not the actual device instance. */
-    bool is_master : 1;
+    bool is_controller : 1;
 
     /* Force setting the MTU actually means first setting the MTU
      * to (desired_MTU-1) and then setting the desired_MTU
@@ -502,11 +502,11 @@ gboolean  nm_device_parent_notify_changed(NMDevice *self,
 const char *nm_device_parent_find_for_connection(NMDevice   *self,
                                                  const char *current_setting_parent);
 
-/* Master */
-gboolean nm_device_is_master(NMDevice *dev);
+/* Controller */
+gboolean nm_device_is_controller(NMDevice *dev);
 
-/* Slave */
-NMDevice *nm_device_get_master(NMDevice *dev);
+/* Port */
+NMDevice *nm_device_get_controller(NMDevice *dev);
 
 NMActRequest          *nm_device_get_act_request(NMDevice *dev);
 NMSettingsConnection  *nm_device_get_settings_connection(NMDevice *dev);
@@ -550,6 +550,7 @@ gboolean nm_device_check_connection_compatible(NMDevice     *device,
                                                GError      **error);
 
 gboolean nm_device_check_slave_connection_compatible(NMDevice *device, NMConnection *connection);
+gboolean nm_device_can_be_parent(NMDevice *device);
 
 gboolean nm_device_can_assume_connections(NMDevice *self);
 gboolean nm_device_unmanage_on_quit(NMDevice *self);
diff --git a/src/core/devices/ovs/nm-device-ovs-bridge.c b/src/core/devices/ovs/nm-device-ovs-bridge.c
index 3bc03ac1..27d04003 100644
--- a/src/core/devices/ovs/nm-device-ovs-bridge.c
+++ b/src/core/devices/ovs/nm-device-ovs-bridge.c
@@ -171,7 +171,7 @@ nm_device_ovs_bridge_class_init(NMDeviceOvsBridgeClass *klass)
     device_class->connection_type_check_compatible = NM_SETTING_OVS_BRIDGE_SETTING_NAME;
     device_class->link_types                       = NM_DEVICE_DEFINE_LINK_TYPES();
 
-    device_class->is_master                           = TRUE;
+    device_class->is_controller                       = TRUE;
     device_class->get_type_description                = get_type_description;
     device_class->create_and_realize                  = create_and_realize;
     device_class->unrealize                           = unrealize;
diff --git a/src/core/devices/ovs/nm-device-ovs-interface.c b/src/core/devices/ovs/nm-device-ovs-interface.c
index 17eb2c2d..778f230b 100644
--- a/src/core/devices/ovs/nm-device-ovs-interface.c
+++ b/src/core/devices/ovs/nm-device-ovs-interface.c
@@ -355,12 +355,12 @@ ovs_interface_is_netdev_datapath(NMDeviceOvsInterface *self)
         return FALSE;
 
     /* get ovs-port active-connection */
-    ac = nm_active_connection_get_master(ac);
+    ac = nm_active_connection_get_controller(ac);
     if (!ac)
         return FALSE;
 
     /* get ovs-bridge active-connection */
-    ac = nm_active_connection_get_master(ac);
+    ac = nm_active_connection_get_controller(ac);
     if (!ac)
         return FALSE;
 
diff --git a/src/core/devices/ovs/nm-device-ovs-port.c b/src/core/devices/ovs/nm-device-ovs-port.c
index 258c72f2..e1020280 100644
--- a/src/core/devices/ovs/nm-device-ovs-port.c
+++ b/src/core/devices/ovs/nm-device-ovs-port.c
@@ -160,7 +160,7 @@ attach_port(NMDevice                  *device,
         return TRUE;
 
     ac_port   = NM_ACTIVE_CONNECTION(nm_device_get_act_request(device));
-    ac_bridge = nm_active_connection_get_master(ac_port);
+    ac_bridge = nm_active_connection_get_controller(ac_port);
     if (!ac_bridge) {
         _LOGW(LOGD_DEVICE,
               "can't attach %s: bridge active-connection not found",
@@ -285,7 +285,7 @@ nm_device_ovs_port_class_init(NMDeviceOvsPortClass *klass)
     device_class->connection_type_check_compatible = NM_SETTING_OVS_PORT_SETTING_NAME;
     device_class->link_types                       = NM_DEVICE_DEFINE_LINK_TYPES();
 
-    device_class->is_master                           = TRUE;
+    device_class->is_controller                       = TRUE;
     device_class->get_type_description                = get_type_description;
     device_class->create_and_realize                  = create_and_realize;
     device_class->get_generic_capabilities            = get_generic_capabilities;
diff --git a/src/core/devices/team/nm-device-team.c b/src/core/devices/team/nm-device-team.c
index d8c3c2c2..1f70537f 100644
--- a/src/core/devices/team/nm-device-team.c
+++ b/src/core/devices/team/nm-device-team.c
@@ -1006,7 +1006,7 @@ get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
 static void
 nm_device_team_init(NMDeviceTeam *self)
 {
-    nm_assert(nm_device_is_master(NM_DEVICE(self)));
+    nm_assert(nm_device_is_controller(NM_DEVICE(self)));
 }
 
 static void
@@ -1127,7 +1127,7 @@ nm_device_team_class_init(NMDeviceTeamClass *klass)
     device_class->connection_type_check_compatible = NM_SETTING_TEAM_SETTING_NAME;
     device_class->link_types                       = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_TEAM);
 
-    device_class->is_master                      = TRUE;
+    device_class->is_controller                  = TRUE;
     device_class->create_and_realize             = create_and_realize;
     device_class->get_generic_capabilities       = get_generic_capabilities;
     device_class->complete_connection            = complete_connection;
diff --git a/src/core/devices/wifi/nm-device-iwd.c b/src/core/devices/wifi/nm-device-iwd.c
index 47407a1e..89444640 100644
--- a/src/core/devices/wifi/nm-device-iwd.c
+++ b/src/core/devices/wifi/nm-device-iwd.c
@@ -774,8 +774,8 @@ check_connection_compatible(NMDevice     *device,
             return FALSE;
         }
 
-        /* Check for MAC address blacklist */
-        mac_blacklist = nm_setting_wireless_get_mac_address_blacklist(s_wireless);
+        /* Check for MAC address denylist */
+        mac_blacklist = nm_setting_wireless_get_mac_address_denylist(s_wireless);
         for (i = 0; mac_blacklist[i]; i++) {
             nm_assert(nm_utils_hwaddr_valid(mac_blacklist[i], ETH_ALEN));
 
diff --git a/src/core/devices/wifi/nm-device-wifi.c b/src/core/devices/wifi/nm-device-wifi.c
index 2cd41b27..206113e4 100644
--- a/src/core/devices/wifi/nm-device-wifi.c
+++ b/src/core/devices/wifi/nm-device-wifi.c
@@ -1013,8 +1013,8 @@ check_connection_compatible(NMDevice     *device,
             return FALSE;
         }
 
-        /* Check for MAC address blacklist */
-        mac_blacklist = nm_setting_wireless_get_mac_address_blacklist(s_wireless);
+        /* Check for MAC address denylist */
+        mac_blacklist = nm_setting_wireless_get_mac_address_denylist(s_wireless);
         for (i = 0; mac_blacklist[i]; i++) {
             if (!nm_utils_hwaddr_valid(mac_blacklist[i], ETH_ALEN)) {
                 g_warn_if_reached();
@@ -3374,7 +3374,7 @@ act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason)
 
     /* Tell the supplicant in which bridge the interface is */
     if ((request = nm_device_get_act_request(device))
-        && (master_ac = nm_active_connection_get_master(NM_ACTIVE_CONNECTION(request)))
+        && (master_ac = nm_active_connection_get_controller(NM_ACTIVE_CONNECTION(request)))
         && (master = nm_active_connection_get_device(master_ac))
         && nm_device_get_device_type(master) == NM_DEVICE_TYPE_BRIDGE) {
         nm_supplicant_interface_set_bridge(priv->sup_iface, nm_device_get_iface(master));
diff --git a/src/core/devices/wifi/nm-wifi-ap.c b/src/core/devices/wifi/nm-wifi-ap.c
index d4d3815e..ceb954b7 100644
--- a/src/core/devices/wifi/nm-wifi-ap.c
+++ b/src/core/devices/wifi/nm-wifi-ap.c
@@ -719,13 +719,14 @@ get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
         g_value_set_uchar(value, priv->strength);
         break;
     case PROP_LAST_SEEN:
-        g_value_set_int(value,
-                        priv->last_seen_msec != G_MININT64 ? (int) NM_MAX(
-                            nm_utils_monotonic_timestamp_as_boottime(priv->last_seen_msec,
-                                                                     NM_UTILS_NSEC_PER_MSEC)
-                                / 1000,
-                            1)
-                                                           : -1);
+        g_value_set_int(
+            value,
+            priv->last_seen_msec != G_MININT64
+                ? (int) NM_MAX(nm_utils_monotonic_timestamp_as_boottime(priv->last_seen_msec,
+                                                                        NM_UTILS_NSEC_PER_MSEC)
+                                   / 1000,
+                               1)
+                : -1);
         break;
     default:
         G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
diff --git a/src/core/devices/wwan/meson.build b/src/core/devices/wwan/meson.build
index 37ef738c..acccb5fd 100644
--- a/src/core/devices/wwan/meson.build
+++ b/src/core/devices/wwan/meson.build
@@ -4,7 +4,7 @@ wwan_inc = include_directories('.')
 
 linker_script = join_paths(meson.current_source_dir(), 'libnm-wwan.ver')
 
-libnm_wwan = shared_module(
+libnm_wwan = shared_library(
   'nm-wwan',
   sources: files(
   'nm-service-providers.c',
@@ -21,6 +21,7 @@ libnm_wwan = shared_module(
   link_depends: linker_script,
   install: true,
   install_dir: nm_plugindir,
+  override_options: ['b_lundef=false'],
 )
 
 libnm_wwan_dep = declare_dependency(
diff --git a/src/core/devices/wwan/nm-modem-broadband.c b/src/core/devices/wwan/nm-modem-broadband.c
index a150040f..c03446b9 100644
--- a/src/core/devices/wwan/nm-modem-broadband.c
+++ b/src/core/devices/wwan/nm-modem-broadband.c
@@ -1155,6 +1155,8 @@ stage3_ip_config_start(NMModem *modem, int addr_family, NMModemIPMethod ip_metho
 #endif
     } else {
         NMPlatformIP6Address address;
+        NMPlatformIP6Address gw;
+        const char          *gw_string;
 
         address_string = mm_bearer_ip_config_get_address(self->_priv.ipv6_config);
         if (!address_string) {
@@ -1165,20 +1167,8 @@ stage3_ip_config_start(NMModem *modem, int addr_family, NMModemIPMethod ip_metho
                             NM_DEVICE_ERROR_INVALID_CONNECTION,
                             "(%s) retrieving IPv6 configuration failed: no address given",
                             nm_modem_get_uid(NM_MODEM(self)));
+                goto out;
             }
-            goto out;
-        }
-
-        address = (NMPlatformIP6Address){};
-
-        if (!inet_pton(AF_INET6, address_string, &address.address)) {
-            g_set_error(&error,
-                        NM_DEVICE_ERROR,
-                        NM_DEVICE_ERROR_INVALID_CONNECTION,
-                        "(%s) retrieving IPv6 configuration failed: invalid address given '%s'",
-                        nm_modem_get_uid(NM_MODEM(self)),
-                        address_string);
-            goto out;
         }
 
         data_port = mm_bearer_get_interface(self->_priv.bearer);
@@ -1202,43 +1192,57 @@ stage3_ip_config_start(NMModem *modem, int addr_family, NMModemIPMethod ip_metho
                                      NM_IP_CONFIG_SOURCE_WWAN);
         do_auto = TRUE;
 
-        address.plen = mm_bearer_ip_config_get_prefix(self->_priv.ipv6_config);
-        if (address.plen <= 128) {
-            if (IN6_IS_ADDR_LINKLOCAL(&address.address)) {
-                nm_utils_ipv6_interface_identifier_get_from_addr(&iid_data, &address.address);
-                iid = &iid_data;
-            } else
-                do_auto = FALSE;
-            nm_l3_config_data_add_address_6(l3cd, &address);
+        if (address_string) {
+            address = (NMPlatformIP6Address){};
+
+            if (!inet_pton(AF_INET6, address_string, &address.address)) {
+                g_set_error(&error,
+                            NM_DEVICE_ERROR,
+                            NM_DEVICE_ERROR_INVALID_CONNECTION,
+                            "(%s) retrieving IPv6 configuration failed: invalid address given '%s'",
+                            nm_modem_get_uid(NM_MODEM(self)),
+                            address_string);
+                goto out;
+            }
+
+            address.plen = mm_bearer_ip_config_get_prefix(self->_priv.ipv6_config);
+            if (address.plen <= 128) {
+                if (IN6_IS_ADDR_LINKLOCAL(&address.address)) {
+                    nm_utils_ipv6_interface_identifier_get_from_addr(&iid_data, &address.address);
+                    iid = &iid_data;
+                } else
+                    do_auto = FALSE;
+                nm_l3_config_data_add_address_6(l3cd, &address);
+            }
+
+            _LOGI("  address %s", nm_platform_ip6_address_to_string(&address, sbuf, sizeof(sbuf)));
         }
 
-        _LOGI("  address %s (slaac %s)",
-              nm_platform_ip6_address_to_string(&address, sbuf, sizeof(sbuf)),
-              do_auto ? "enabled" : "disabled");
+        _LOGI("  slaac %s", do_auto ? "enabled" : "disabled");
 
-        address_string = mm_bearer_ip_config_get_gateway(self->_priv.ipv6_config);
-        if (address_string) {
-            if (inet_pton(AF_INET6, address_string, &address.address) != 1) {
+        gw_string = mm_bearer_ip_config_get_gateway(self->_priv.ipv6_config);
+        if (gw_string) {
+            if (inet_pton(AF_INET6, gw_string, &gw.address) != 1) {
                 g_set_error(&error,
                             NM_DEVICE_ERROR,
                             NM_DEVICE_ERROR_INVALID_CONNECTION,
                             "(%s) retrieving IPv6 configuration failed: invalid gateway given '%s'",
                             nm_modem_get_uid(NM_MODEM(self)),
-                            address_string);
+                            gw_string);
                 goto out;
             }
 
             {
                 const NMPlatformIP6Route r = {
                     .rt_source     = NM_IP_CONFIG_SOURCE_WWAN,
-                    .gateway       = address.address,
+                    .gateway       = gw.address,
                     .table_any     = TRUE,
                     .table_coerced = 0,
                     .metric_any    = TRUE,
                     .metric        = 0,
                 };
 
-                _LOGI("  gateway %s", address_string);
+                _LOGI("  gateway %s", gw_string);
                 nm_l3_config_data_add_route_6(l3cd, &r);
             }
         } else if (ip_method == NM_MODEM_IP_METHOD_STATIC) {
diff --git a/src/core/dhcp/README.next.md b/src/core/dhcp/README.next.md
new file mode 100644
index 00000000..88fa6683
--- /dev/null
+++ b/src/core/dhcp/README.next.md
@@ -0,0 +1,103 @@
+`NMDhcpClient`
+==============
+
+Using `NMDhcpClient` still requires a lot of logic in `NMDevice`. The main goal
+is to simplify `NMDevice`, so `NMDhcpClient` must become more complicated to
+provide a simpler (but robust) API.
+
+NMDevice has basically two timeouts (talking about IPv4, but it applies
+similarly to IPv6): `ipv4.dhcp-timeout` and `ipv4.required-timeout`. They
+control how long NMDevice is willing to try, before failing the activation
+altogether. Note that with `ipv4.may-fail=yes`, we may very well never want to
+fail the activation entirely, regardless how DHCP is doing. In that case we
+want to stay up, but also constantly retrying whether we cannot get a lease and
+recover.
+
+Currently, if `NMDhcpClient` signals a failure, then it's basically up to
+`NMDevice` to schedule and retry. That is complicated, and we should move the
+complexity out of `NMDevice`.
+
+`NMDhcpClient` should have a simpler API:
+
+- `nm_dhcp_manager_start_ip[46]()`: creates (and starts) a `NMDhcpClient`
+  instance. The difference is, this function tries really hard not to fail
+  to create an `NMDhcpClient`. There is no explicit `start()`, but note that the
+  instance must not emit any signals before the next maincontext iteration. That is,
+  it only will call back the user after a timeout/idle or some other IO event, which
+  happens during a future iteration of the maincontext.
+
+- `nm_dhcp_client_stop()`: when `NMDevice` is done with the `NMDhcpClient`
+  instance, it will stop it and throw it away. This method exists because
+  `NMDhcpClient` is a `GObject` and ref-counted. Thus, we don't want to rely on
+  the last unref to stop the instance, but have an explicit stop. After stop, the
+  instance is defunct and won't emit any signals anymore. The class does not need
+  to support restarting a stopped instance. If `NMDevice` wants to restart DHCP, it
+  should create a new one. `NMDevice` would only want to do that, if the parameters
+  change, hence a new instance is in order (and no need for the complexity of
+  restart in `NMDhcpClient`).
+
+- as already now, `NMDhcpClient` is not very configurable. You provide most
+  (all) parameters during `nm_dhcp_manager_start_ip[46]()`, and then it keeps
+  running until stop.
+
+- `NMDhcpClient` exposes a simple state to the user:
+
+   1. "no lease, but good". When starting, there is no lease, but we are
+      optimistic to get one. This is the inital state, but we can also get back to
+      this state after we had a lease (which might expire).
+
+   1. "has a lease". Here there is no need to distinguish whether the current
+      lease was the first we received, or whether this was an update. In this state,
+      the instance has a lease and we are good.
+
+   1. "no lease, but bad". `NMDhcpClient` tries really hard, and "bad" does not
+      mean that it gave up. It will keep retrying, it's just that there is little
+      hope of getting a new lease. This happens, when you try to run DHCP on a Layer3
+      link (WireGuard). There is little hope to succeed, but `NMDhcpClient`
+      (theoretically) will retry and may recover from this. Another example is when
+      we fail to start dhclient because it's not installed. In that case, we are not
+      optimistic to recover, however `NMDhcpDhclient` will retry (with backoff
+      timeout) and might still recover from this. For most cases, `NMDevice` will
+      treat the no-lease cases the same, but in case of "bad" it might give up
+      earlier.
+
+When a lease expires, that does not necessarily mean that we are now in a bad
+state. It might mean that the DHCP server is temporarily down, but we might
+recover from that easily. "bad" really means, something is wrong on our side
+which prevents us from getting a lease. Also, imagine `dhclient` dies (we would
+try to restart, but assume that fails too), but we still have a valid lease,
+then possibly `NMDhcpClient` should still pretend all is good and we still have
+a lease until it expires. It may be we can recover before that happens. The
+point of all of this, is to hide errors as much as possibly and automatically
+recover. `NMDevice` will decide to tear down, if we didn't get a lease after
+`ipv4.dhcp-timeout`. That's the main criteria, and it might not even
+distinguish between "no lease, but good" and "no lease, but bad".
+
+- `NMDhcpClient` will also take care of the `ipv4.dhcp-timeout` grace period.
+  That timeout is provided during start, and starts ticking whenever there is
+  no lease. When it expires, a timeout signal gets emitted. That's it. This is
+  independent from the 3 states above, and only saves `NMDevice` from scheduling
+  this timer themselves.
+  This is NM_DHCP_CLIENT_NOTIFY_TYPE_NO_LEASE_TIMEOUT notification.
+
+- for nettools, `nm_dhcp_client_can_accept()` indicates that when we receive a
+  lease, we need to accept/decline it first. In that case, `NMDevice`
+optionally does ACD first, then configures the IP address first and calls
+`nm_dhcp_client_accept()`. In case of ACD conflict, it will call
+`nm_dhcp_client_decline()` (which optimally causes `NMDhcpClient` to get a
+different lease). With this, the above state "has a lease" has actually three
+flavors: "has a lease but not yet ACD probed" and "has a lease but
+accepted/declined" (but `NM_DHCP_CLIENT_SIGNAL_STATE_CHANGED` gets only emitted
+when we get the lease, not when we accept/decline it). With `dhclient`, when we
+receive a lease, it means  "has a lease but accepted" right away.
+
+- for IPv6 prefix delegation, there is also `needed_prefixes` and
+  `NM_DHCP_CLIENT_NOTIFY_TYPE_PREFIX_DELEGATED`. Currently `needed_prefixes` needs
+  to be specified during start (which simplifies things). Maybe `needed_prefixes`
+  should be changable at runtime. Otherwise, whether we have prefixes is similar
+  to whether we have a lease, and the simple 3 states apply.
+
+When NetworkManager quits, it may want to leave the interface up. In that case,
+we still always want to stop the DHCP client, but possibly not deconfiguring
+the interface. I don't think that this concerns `NMDhcpClient`, because `NMDhcpClient`
+only provides the lease information and `NMDevice` is responsible to configure it.
diff --git a/src/core/dhcp/nm-dhcp-client.c b/src/core/dhcp/nm-dhcp-client.c
index 8770656b..4ebc1754 100644
--- a/src/core/dhcp/nm-dhcp-client.c
+++ b/src/core/dhcp/nm-dhcp-client.c
@@ -824,9 +824,10 @@ _nm_dhcp_client_notify(NMDhcpClient         *self,
 
     _acd_check_lease(self, &acd_state);
 
-    options = priv->l3cd_next ? nm_dhcp_lease_get_options(
-                  nm_l3_config_data_get_dhcp_lease(priv->l3cd_next, priv->config.addr_family))
-                              : NULL;
+    options = priv->l3cd_next
+                  ? nm_dhcp_lease_get_options(
+                        nm_l3_config_data_get_dhcp_lease(priv->l3cd_next, priv->config.addr_family))
+                  : NULL;
 
     if (_LOGI_ENABLED()) {
         const char *req_str =
diff --git a/src/core/dhcp/nm-dhcp-nettools.c b/src/core/dhcp/nm-dhcp-nettools.c
index ce1e9a45..b81ce77a 100644
--- a/src/core/dhcp/nm-dhcp-nettools.c
+++ b/src/core/dhcp/nm-dhcp-nettools.c
@@ -1483,6 +1483,11 @@ stop(NMDhcpClient *client, gboolean release)
     NMDhcpNettools        *self = NM_DHCP_NETTOOLS(client);
     NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE(self);
 
+    if (release) {
+        if (n_dhcp4_client_probe_release(priv->probe))
+            _LOGT("dhcp-client4: failed to send request with RELEASE message");
+    }
+
     NM_DHCP_CLIENT_CLASS(nm_dhcp_nettools_parent_class)->stop(client, release);
 
     _LOGT("dhcp-client4: stop " NM_HASH_OBFUSCATE_PTR_FMT, NM_HASH_OBFUSCATE_PTR(priv->client));
diff --git a/src/core/dhcp/nm-dhcp-systemd.c b/src/core/dhcp/nm-dhcp-systemd.c
index 0fc5f928..5ede0df9 100644
--- a/src/core/dhcp/nm-dhcp-systemd.c
+++ b/src/core/dhcp/nm-dhcp-systemd.c
@@ -67,6 +67,15 @@ G_DEFINE_TYPE(NMDhcpSystemd, nm_dhcp_systemd, NM_TYPE_DHCP_CLIENT)
 
 /*****************************************************************************/
 
+static guint32
+lifetime_to_uint32(guint64 lft)
+{
+    if (lft == G_MAXUINT64)
+        return G_MAXUINT32;
+
+    return lft / 1000000;
+}
+
 static NML3ConfigData *
 lease_to_ip6_config(NMDhcpSystemd *self, sd_dhcp6_lease *lease, gint32 ts, GError **error)
 {
@@ -100,18 +109,19 @@ lease_to_ip6_config(NMDhcpSystemd *self, sd_dhcp6_lease *lease, gint32 ts, GErro
 
     if (!config->v6.info_only) {
         gboolean has_any_addresses = FALSE;
-        uint32_t lft_pref;
-        uint32_t lft_valid;
+        uint64_t lft_pref;
+        uint64_t lft_valid;
 
-        sd_dhcp6_lease_reset_address_iter(lease);
+        sd_dhcp6_lease_address_iterator_reset(lease);
         nm_gstring_prepare(&str);
-        while (sd_dhcp6_lease_get_address(lease, &tmp_addr, &lft_pref, &lft_valid) >= 0) {
-            const NMPlatformIP6Address address = {
+        while (sd_dhcp6_lease_get_address(lease, &tmp_addr) >= 0
+               && sd_dhcp6_lease_get_address_lifetime(lease, &lft_pref, &lft_valid) >= 0) {
+            NMPlatformIP6Address address = {
                 .plen        = 128,
                 .address     = tmp_addr,
                 .timestamp   = ts,
-                .lifetime    = lft_valid,
-                .preferred   = lft_pref,
+                .lifetime    = lifetime_to_uint32(lft_valid),
+                .preferred   = lifetime_to_uint32(lft_pref),
                 .addr_source = NM_IP_CONFIG_SOURCE_DHCP,
             };
 
@@ -121,6 +131,7 @@ lease_to_ip6_config(NMDhcpSystemd *self, sd_dhcp6_lease *lease, gint32 ts, GErro
             g_string_append(nm_gstring_add_space_delimiter(str), addr_str);
 
             has_any_addresses = TRUE;
+            sd_dhcp6_lease_address_iterator_next(lease);
         }
 
         if (str->len) {
@@ -160,11 +171,12 @@ lease_to_ip6_config(NMDhcpSystemd *self, sd_dhcp6_lease *lease, gint32 ts, GErro
         uint8_t         prefix_len;
 
         nm_gstring_prepare(&str);
-        sd_dhcp6_lease_reset_pd_prefix_iter(lease);
-        while (!sd_dhcp6_lease_get_pd(lease, &prefix, &prefix_len, NULL, NULL)) {
+        sd_dhcp6_lease_pd_iterator_reset(lease);
+        while (!sd_dhcp6_lease_get_pd_prefix(lease, &prefix, &prefix_len)) {
             nm_gstring_add_space_delimiter(str);
             nm_inet6_ntop(&prefix, addr_str);
             g_string_append_printf(str, "%s/%u", addr_str, prefix_len);
+            sd_dhcp6_lease_pd_iterator_next(lease);
         }
         if (str->len > 0) {
             nm_dhcp_option_add_option(options,
@@ -235,6 +247,8 @@ bound6_handle(NMDhcpSystemd *self)
     gs_free_error GError                   *error  = NULL;
     NMPlatformIP6Address                    prefix = {0};
     sd_dhcp6_lease                         *lease  = NULL;
+    guint64                                 lft_valid;
+    guint64                                 lft_pref;
 
     if (sd_dhcp6_client_get_lease(priv->client6, &lease) < 0 || !lease) {
         _LOGW(" no lease!");
@@ -254,14 +268,14 @@ bound6_handle(NMDhcpSystemd *self)
 
     _nm_dhcp_client_notify(NM_DHCP_CLIENT(self), NM_DHCP_CLIENT_EVENT_TYPE_BOUND, l3cd);
 
-    sd_dhcp6_lease_reset_pd_prefix_iter(lease);
-    while (!sd_dhcp6_lease_get_pd(lease,
-                                  &prefix.address,
-                                  &prefix.plen,
-                                  &prefix.preferred,
-                                  &prefix.lifetime)) {
+    sd_dhcp6_lease_pd_iterator_reset(lease);
+    while (!sd_dhcp6_lease_get_pd_prefix(lease, &prefix.address, &prefix.plen)
+           && !sd_dhcp6_lease_get_pd_lifetime(lease, &lft_pref, &lft_valid)) {
+        prefix.preferred = lifetime_to_uint32(lft_pref);
+        prefix.lifetime  = lifetime_to_uint32(lft_valid);
         prefix.timestamp = ts;
         nm_dhcp_client_emit_ipv6_prefix_delegated(NM_DHCP_CLIENT(self), &prefix);
+        sd_dhcp6_lease_pd_iterator_next(lease);
     }
 }
 
@@ -339,10 +353,10 @@ ip6_start(NMDhcpClient *client, const struct in6_addr *ll_addr, GError **error)
         return FALSE;
     }
 
-    r = sd_dhcp6_client_set_duid(sd_client,
-                                 unaligned_read_be16(&duid_arr[0]),
-                                 &duid_arr[2],
-                                 duid_len - 2);
+    r = sd_dhcp6_client_set_duid_raw(sd_client,
+                                     unaligned_read_be16(&duid_arr[0]),
+                                     &duid_arr[2],
+                                     duid_len - 2);
     if (r < 0) {
         nm_utils_error_set_errno(error, r, "failed to set DUID: %s");
         return FALSE;
@@ -450,6 +464,10 @@ stop(NMDhcpClient *client, gboolean release)
     if (!priv->client6)
         return;
 
+    r = sd_dhcp6_client_set_send_release(priv->client6, release);
+    if (r)
+        _LOGT("dhcp-client6: failed setting send-release");
+
     sd_dhcp6_client_set_callback(priv->client6, NULL, NULL);
     r = sd_dhcp6_client_stop(priv->client6);
     if (r)
diff --git a/src/core/meson.build b/src/core/meson.build
index 45b068a6..4419ff62 100644
--- a/src/core/meson.build
+++ b/src/core/meson.build
@@ -171,7 +171,7 @@ libNetworkManager = static_library(
     'nm-policy.c',
     'nm-rfkill-manager.c',
     'nm-session-monitor.c',
-    'nm-sleep-monitor.c',
+    'nm-power-monitor.c',
     'nm-priv-helper-call.c',
   ),
   dependencies: nm_deps,
diff --git a/src/core/ndisc/nm-ndisc.c b/src/core/ndisc/nm-ndisc.c
index e6b1a94e..ca646a6d 100644
--- a/src/core/ndisc/nm-ndisc.c
+++ b/src/core/ndisc/nm-ndisc.c
@@ -1853,6 +1853,7 @@ _config_init(NMNDiscConfig *config, const NMNDiscConfig *src)
     g_return_if_fail(
         NM_IN_SET(config->node_type, NM_NDISC_NODE_TYPE_HOST, NM_NDISC_NODE_TYPE_ROUTER));
     g_return_if_fail(NM_IN_SET(config->ip6_privacy,
+                               NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN,
                                NM_SETTING_IP6_CONFIG_PRIVACY_DISABLED,
                                NM_SETTING_IP6_CONFIG_PRIVACY_PREFER_PUBLIC_ADDR,
                                NM_SETTING_IP6_CONFIG_PRIVACY_PREFER_TEMP_ADDR));
diff --git a/src/core/ndisc/nm-ndisc.h b/src/core/ndisc/nm-ndisc.h
index 8f1a12a2..b8f8b06e 100644
--- a/src/core/ndisc/nm-ndisc.h
+++ b/src/core/ndisc/nm-ndisc.h
@@ -40,7 +40,7 @@ typedef enum {
 
 const char *nm_ndisc_dhcp_level_to_string(NMNDiscDHCPLevel level);
 
-#define NM_NDISC_INFINITY_U32 ((uint32_t) -1)
+#define NM_NDISC_INFINITY_U32 ((uint32_t) - 1)
 
 /* It's important that this is G_MAXINT64, so that we can meaningfully do
  * MIN(e1, e2) to find the minimum expiry time (and properly handle if any
diff --git a/src/core/nm-active-connection.c b/src/core/nm-active-connection.c
index eb7b1cca..b08d26c2 100644
--- a/src/core/nm-active-connection.c
+++ b/src/core/nm-active-connection.c
@@ -38,7 +38,7 @@ typedef struct _NMActiveConnectionPrivate {
     bool                    is_default6 : 1;
     bool                    state_set : 1;
     bool                    vpn : 1;
-    bool                    master_ready : 1;
+    bool                    controller_ready : 1;
 
     NMActivationType activation_type : 3;
 
@@ -49,7 +49,7 @@ typedef struct _NMActiveConnectionPrivate {
     NMActivationReason activation_reason : 4;
 
     NMAuthSubject      *subject;
-    NMActiveConnection *master;
+    NMActiveConnection *controller;
 
     NMActiveConnection *parent;
 
@@ -87,8 +87,8 @@ NM_GOBJECT_PROPERTIES_DEFINE(NMActiveConnection,
                              PROP_INT_APPLIED_CONNECTION,
                              PROP_INT_DEVICE,
                              PROP_INT_SUBJECT,
-                             PROP_INT_MASTER,
-                             PROP_INT_MASTER_READY,
+                             PROP_INT_CONTROLLER,
+                             PROP_INT_CONTROLLER_READY,
                              PROP_INT_ACTIVATION_TYPE,
                              PROP_INT_ACTIVATION_REASON, );
 
@@ -105,7 +105,7 @@ G_DEFINE_ABSTRACT_TYPE(NMActiveConnection, nm_active_connection, NM_TYPE_DBUS_OB
 static const NMDBusInterfaceInfoExtended interface_info_active_connection;
 static const GDBusSignalInfo             signal_info_state_changed;
 
-static void check_master_ready(NMActiveConnection *self);
+static void check_controller_ready(NMActiveConnection *self);
 static void _device_cleanup(NMActiveConnection *self);
 static void _settings_connection_flags_changed(NMSettingsConnection *settings_connection,
                                                NMActiveConnection   *self);
@@ -276,7 +276,7 @@ nm_active_connection_set_state(NMActiveConnection           *self,
     emit_state_changed(self, new_state, reason);
     _notify(self, PROP_STATE);
 
-    check_master_ready(self);
+    check_controller_ready(self);
 
     if (new_state == NM_ACTIVE_CONNECTION_STATE_ACTIVATED
         || old_state == NM_ACTIVE_CONNECTION_STATE_ACTIVATED) {
@@ -446,7 +446,7 @@ _set_applied_connection_take(NMActiveConnection *self, NMConnection *applied_con
     /* we determine whether the connection is a master/slave, based solely
      * on the connection properties itself. */
     s_con = nm_connection_get_setting_connection(priv->applied_connection);
-    if (nm_setting_connection_get_master(s_con))
+    if (nm_setting_connection_get_controller(s_con))
         flags_val |= NM_ACTIVATION_STATE_FLAG_IS_SLAVE;
 
     if (_nm_connection_type_is_master(nm_setting_connection_get_connection_type(s_con)))
@@ -650,13 +650,13 @@ device_master_changed(GObject *object, GParamSpec *pspec, gpointer user_data)
 
     if (NM_ACTIVE_CONNECTION(nm_device_get_act_request(device)) != self)
         return;
-    if (!nm_device_get_master(device))
+    if (!nm_device_get_controller(device))
         return;
-    if (!nm_active_connection_get_master(self))
+    if (!nm_active_connection_get_controller(self))
         return;
     g_signal_handlers_disconnect_by_func(device, G_CALLBACK(device_master_changed), self);
 
-    master       = nm_active_connection_get_master(self);
+    master       = nm_active_connection_get_controller(self);
     master_state = nm_active_connection_get_state(master);
     if (master_state >= NM_ACTIVE_CONNECTION_STATE_DEACTIVATING) {
         /* Master failed before attaching the slave */
@@ -704,15 +704,15 @@ nm_active_connection_set_device(NMActiveConnection *self, NMDevice *device)
 
     if (device) {
         /* Device obviously can't be its own master */
-        g_return_val_if_fail(!priv->master
-                                 || device != nm_active_connection_get_device(priv->master),
+        g_return_val_if_fail(!priv->controller
+                                 || device != nm_active_connection_get_device(priv->controller),
                              FALSE);
 
         priv->device = g_object_ref(device);
 
         g_signal_connect(device, NM_DEVICE_STATE_CHANGED, G_CALLBACK(device_state_changed), self);
         g_signal_connect(device,
-                         "notify::" NM_DEVICE_MASTER,
+                         "notify::" NM_DEVICE_CONTROLLER,
                          G_CALLBACK(device_master_changed),
                          self);
         g_signal_connect(device,
@@ -747,30 +747,30 @@ nm_active_connection_set_device(NMActiveConnection *self, NMDevice *device)
 }
 
 NMActiveConnection *
-nm_active_connection_get_master(NMActiveConnection *self)
+nm_active_connection_get_controller(NMActiveConnection *self)
 {
     g_return_val_if_fail(NM_IS_ACTIVE_CONNECTION(self), NULL);
 
-    return NM_ACTIVE_CONNECTION_GET_PRIVATE(self)->master;
+    return NM_ACTIVE_CONNECTION_GET_PRIVATE(self)->controller;
 }
 
 /**
- * nm_active_connection_get_master_ready:
+ * nm_active_connection_get_controller_ready:
  * @self: the #NMActiveConnection
  *
- * Returns: %TRUE if the connection has a master connection, and that
- * master connection is ready to accept slaves.  Otherwise, %FALSE.
+ * Returns: %TRUE if the connection has a controller connection, and that
+ * controller connection is ready to accept ports.  Otherwise, %FALSE.
  */
 gboolean
-nm_active_connection_get_master_ready(NMActiveConnection *self)
+nm_active_connection_get_controller_ready(NMActiveConnection *self)
 {
     g_return_val_if_fail(NM_IS_ACTIVE_CONNECTION(self), FALSE);
 
-    return NM_ACTIVE_CONNECTION_GET_PRIVATE(self)->master_ready;
+    return NM_ACTIVE_CONNECTION_GET_PRIVATE(self)->controller_ready;
 }
 
 static void
-check_master_ready(NMActiveConnection *self)
+check_controller_ready(NMActiveConnection *self)
 {
     NMActiveConnectionPrivate *priv       = NM_ACTIVE_CONNECTION_GET_PRIVATE(self);
     gboolean                   signalling = FALSE;
@@ -780,30 +780,31 @@ check_master_ready(NMActiveConnection *self)
      * device will be ready to accept slaves when the master is in ACTIVATING
      * or higher states.
      */
-    if (!priv->master_ready && priv->master && priv->state == NM_ACTIVE_CONNECTION_STATE_ACTIVATING
-        && NM_IN_SET(nm_active_connection_get_state(priv->master),
+    if (!priv->controller_ready && priv->controller
+        && priv->state == NM_ACTIVE_CONNECTION_STATE_ACTIVATING
+        && NM_IN_SET(nm_active_connection_get_state(priv->controller),
                      NM_ACTIVE_CONNECTION_STATE_ACTIVATING,
                      NM_ACTIVE_CONNECTION_STATE_ACTIVATED)) {
         signalling = TRUE;
     }
 
-    _LOGD("check-master-ready: %s (state %s, %s)",
-          signalling ? "signal" : (priv->master_ready ? "already signalled" : "not signalling"),
+    _LOGD("check-controller-ready: %s (state %s, %s)",
+          signalling ? "signal" : (priv->controller_ready ? "already signalled" : "not signalling"),
           state_to_string_a(priv->state),
-          priv->master
+          priv->controller
               ? nm_sprintf_bufa(128,
-                                "master %p is in state %s",
-                                priv->master,
-                                state_to_string_a(nm_active_connection_get_state(priv->master)))
-              : "no master");
+                                "controller %p is in state %s",
+                                priv->controller,
+                                state_to_string_a(nm_active_connection_get_state(priv->controller)))
+              : "no controller");
 
     if (signalling) {
-        priv->master_ready = TRUE;
-        _notify(self, PROP_INT_MASTER_READY);
+        priv->controller_ready = TRUE;
+        _notify(self, PROP_INT_CONTROLLER_READY);
 
-        /* Also notify clients to recheck the exported 'master' property to
-         * ensure that if the master connection was created without a device
-         * that we notify clients when the master device is known.
+        /* Also notify clients to recheck the exported 'controller' property to
+         * ensure that if the controller connection was created without a device
+         * that we notify clients when the controller device is known.
          */
         nm_gobject_notify_together(self, PROP_MASTER, PROP_CONTROLLER);
     }
@@ -816,9 +817,9 @@ master_state_cb(NMActiveConnection *master, GParamSpec *pspec, gpointer user_dat
     NMActiveConnectionPrivate *priv         = NM_ACTIVE_CONNECTION_GET_PRIVATE(self);
     NMActiveConnectionState    master_state = nm_active_connection_get_state(master);
 
-    check_master_ready(self);
+    check_controller_ready(self);
 
-    if (master_state == NM_ACTIVE_CONNECTION_STATE_DEACTIVATING && !priv->master_ready) {
+    if (master_state == NM_ACTIVE_CONNECTION_STATE_DEACTIVATING && !priv->controller_ready) {
         /* Master disconnected before the slave was added */
         if (NM_ACTIVE_CONNECTION_GET_CLASS(self)->master_failed)
             NM_ACTIVE_CONNECTION_GET_CLASS(self)->master_failed(self);
@@ -826,44 +827,44 @@ master_state_cb(NMActiveConnection *master, GParamSpec *pspec, gpointer user_dat
 }
 
 /**
- * nm_active_connection_set_master:
+ * nm_active_connection_set_controller:
  * @self: the #NMActiveConnection
- * @master: if the activation depends on another device (ie, bond or bridge
- * master to which this device will be enslaved) pass the #NMActiveConnection
+ * @controller: if the activation depends on another device (ie, bond or bridge
+ * controller to which this device will be set as port) pass the #NMActiveConnection
  * that this activation request is a child of
  *
- * Sets the master active connection of @self.
+ * Sets the controller active connection of @self.
  */
 void
-nm_active_connection_set_master(NMActiveConnection *self, NMActiveConnection *master)
+nm_active_connection_set_controller(NMActiveConnection *self, NMActiveConnection *controller)
 {
     NMActiveConnectionPrivate *priv;
 
     g_return_if_fail(NM_IS_ACTIVE_CONNECTION(self));
-    g_return_if_fail(NM_IS_ACTIVE_CONNECTION(master));
+    g_return_if_fail(NM_IS_ACTIVE_CONNECTION(controller));
 
     priv = NM_ACTIVE_CONNECTION_GET_PRIVATE(self);
 
-    /* Master is write-once, and must be set before exporting the object */
-    g_return_if_fail(priv->master == NULL);
+    /* Controller is write-once, and must be set before exporting the object */
+    g_return_if_fail(priv->controller == NULL);
     g_return_if_fail(!nm_dbus_object_is_exported(NM_DBUS_OBJECT(self)));
     if (priv->device) {
         /* Note, the master ActiveConnection may not yet have a device */
-        g_return_if_fail(priv->device != nm_active_connection_get_device(master));
+        g_return_if_fail(priv->device != nm_active_connection_get_device(controller));
     }
 
-    _LOGD("set master %p, %s, state %s",
-          master,
-          nm_active_connection_get_settings_connection_id(master),
-          state_to_string_a(nm_active_connection_get_state(master)));
+    _LOGD("set controller %p, %s, state %s",
+          controller,
+          nm_active_connection_get_settings_connection_id(controller),
+          state_to_string_a(nm_active_connection_get_state(controller)));
 
-    priv->master = g_object_ref(master);
-    g_signal_connect(priv->master,
+    priv->controller = g_object_ref(controller);
+    g_signal_connect(priv->controller,
                      "notify::" NM_ACTIVE_CONNECTION_STATE,
                      G_CALLBACK(master_state_cb),
                      self);
 
-    check_master_ready(self);
+    check_controller_ready(self);
 }
 
 NMActivationType
@@ -1347,15 +1348,15 @@ get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
         break;
     case PROP_CONTROLLER:
     case PROP_MASTER:
-        if (priv->master)
-            master_device = nm_active_connection_get_device(priv->master);
+        if (priv->controller)
+            master_device = nm_active_connection_get_device(priv->controller);
         nm_dbus_utils_g_value_set_object_path(value, master_device);
         break;
     case PROP_INT_SUBJECT:
         g_value_set_object(value, priv->subject);
         break;
-    case PROP_INT_MASTER_READY:
-        g_value_set_boolean(value, priv->master_ready);
+    case PROP_INT_CONTROLLER_READY:
+        g_value_set_boolean(value, priv->controller_ready);
         break;
     default:
         G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
@@ -1401,8 +1402,8 @@ set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *ps
         /* construct-only */
         priv->subject = g_value_dup_object(value);
         break;
-    case PROP_INT_MASTER:
-        nm_active_connection_set_master(self, g_value_get_object(value));
+    case PROP_INT_CONTROLLER:
+        nm_active_connection_set_controller(self, g_value_get_object(value));
         break;
     case PROP_INT_ACTIVATION_TYPE:
         /* construct-only */
@@ -1529,10 +1530,10 @@ dispose(GObject *object)
 
     _device_cleanup(self);
 
-    if (priv->master) {
-        g_signal_handlers_disconnect_by_func(priv->master, G_CALLBACK(master_state_cb), self);
+    if (priv->controller) {
+        g_signal_handlers_disconnect_by_func(priv->controller, G_CALLBACK(master_state_cb), self);
     }
-    g_clear_object(&priv->master);
+    g_clear_object(&priv->controller);
 
     if (priv->parent)
         unwatch_parent(self, TRUE);
@@ -1774,15 +1775,15 @@ nm_active_connection_class_init(NMActiveConnectionClass *ac_class)
                             NM_TYPE_AUTH_SUBJECT,
                             G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
 
-    obj_properties[PROP_INT_MASTER] =
-        g_param_spec_object(NM_ACTIVE_CONNECTION_INT_MASTER,
+    obj_properties[PROP_INT_CONTROLLER] =
+        g_param_spec_object(NM_ACTIVE_CONNECTION_INT_CONTROLLER,
                             "",
                             "",
                             NM_TYPE_ACTIVE_CONNECTION,
                             G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
 
-    obj_properties[PROP_INT_MASTER_READY] =
-        g_param_spec_boolean(NM_ACTIVE_CONNECTION_INT_MASTER_READY,
+    obj_properties[PROP_INT_CONTROLLER_READY] =
+        g_param_spec_boolean(NM_ACTIVE_CONNECTION_INT_CONTROLLER_READY,
                              "",
                              "",
                              FALSE,
diff --git a/src/core/nm-active-connection.h b/src/core/nm-active-connection.h
index 8032294f..12cb311c 100644
--- a/src/core/nm-active-connection.h
+++ b/src/core/nm-active-connection.h
@@ -45,8 +45,8 @@
 #define NM_ACTIVE_CONNECTION_INT_APPLIED_CONNECTION  "int-applied-connection"
 #define NM_ACTIVE_CONNECTION_INT_DEVICE              "int-device"
 #define NM_ACTIVE_CONNECTION_INT_SUBJECT             "int-subject"
-#define NM_ACTIVE_CONNECTION_INT_MASTER              "int-master"
-#define NM_ACTIVE_CONNECTION_INT_MASTER_READY        "int-master-ready"
+#define NM_ACTIVE_CONNECTION_INT_CONTROLLER          "int-controller"
+#define NM_ACTIVE_CONNECTION_INT_CONTROLLER_READY    "int-controller-ready"
 #define NM_ACTIVE_CONNECTION_INT_ACTIVATION_TYPE     "int-activation-type"
 #define NM_ACTIVE_CONNECTION_INT_ACTIVATION_REASON   "int-activation-reason"
 
@@ -170,11 +170,11 @@ NMAuthSubject *nm_active_connection_get_subject(NMActiveConnection *self);
 
 gboolean nm_active_connection_get_user_requested(NMActiveConnection *self);
 
-NMActiveConnection *nm_active_connection_get_master(NMActiveConnection *self);
+NMActiveConnection *nm_active_connection_get_controller(NMActiveConnection *self);
 
-gboolean nm_active_connection_get_master_ready(NMActiveConnection *self);
+gboolean nm_active_connection_get_controller_ready(NMActiveConnection *self);
 
-void nm_active_connection_set_master(NMActiveConnection *self, NMActiveConnection *master);
+void nm_active_connection_set_controller(NMActiveConnection *self, NMActiveConnection *controller);
 
 void nm_active_connection_set_parent(NMActiveConnection *self, NMActiveConnection *parent);
 
diff --git a/src/core/nm-audit-manager.c b/src/core/nm-audit-manager.c
index 4e134d1a..7cf52946 100644
--- a/src/core/nm-audit-manager.c
+++ b/src/core/nm-audit-manager.c
@@ -185,13 +185,16 @@ nm_audit_log(NMAuditManager *self,
     priv = NM_AUDIT_MANAGER_GET_PRIVATE(self);
 
     if (priv->auditd_fd >= 0) {
-        audit_log_user_message(priv->auditd_fd,
-                               AUDIT_USYS_CONFIG,
-                               build_message(&strbuf, BACKEND_AUDITD, fields),
-                               NULL,
-                               NULL,
-                               NULL,
-                               success);
+        int r;
+
+        r = audit_log_user_message(priv->auditd_fd,
+                                   AUDIT_USYS_CONFIG,
+                                   build_message(&strbuf, BACKEND_AUDITD, fields),
+                                   NULL,
+                                   NULL,
+                                   NULL,
+                                   success);
+        (void) r;
     }
 #endif
 
diff --git a/src/core/nm-checkpoint.c b/src/core/nm-checkpoint.c
index 74adf484..ffcf6e3a 100644
--- a/src/core/nm-checkpoint.c
+++ b/src/core/nm-checkpoint.c
@@ -10,12 +10,15 @@
 #include "nm-active-connection.h"
 #include "nm-act-request.h"
 #include "libnm-core-aux-intern/nm-auth-subject.h"
+#include "libnm-core-intern/nm-keyfile-internal.h"
 #include "nm-core-utils.h"
 #include "nm-dbus-interface.h"
 #include "devices/nm-device.h"
+#include "nm-config.h"
 #include "nm-manager.h"
 #include "settings/nm-settings.h"
 #include "settings/nm-settings-connection.h"
+#include "settings/plugins/keyfile/nms-keyfile-storage.h"
 #include "nm-simple-connection.h"
 #include "nm-utils.h"
 
@@ -28,11 +31,14 @@ typedef struct {
     NMDevice          *device;
     NMConnection      *applied_connection;
     NMConnection      *settings_connection;
+    NMConnection      *settings_connection_shadowed;
     guint64            ac_version_id;
     NMDeviceState      state;
     bool               is_software : 1;
     bool               realized : 1;
     bool               activation_lifetime_bound_to_profile_visibility : 1;
+    bool               settings_connection_is_unsaved : 1;
+    bool               settings_connection_is_shadowed_owned : 1;
     NMUnmanFlagOp      unmanaged_explicit;
     NMActivationReason activation_reason;
     gulong             dev_exported_change_id;
@@ -55,6 +61,8 @@ struct _NMCheckpointPrivate {
 
     NMCheckpointTimeoutCallback timeout_cb;
     gpointer                    timeout_data;
+
+    NMGlobalDnsConfig *global_dns_config;
 };
 
 struct _NMCheckpointClass {
@@ -147,37 +155,111 @@ nm_checkpoint_includes_devices_of(NMCheckpoint *self, NMCheckpoint *cp_for_devic
     return NULL;
 }
 
+static NMConnection *
+parse_connection_from_shadowed_file(const char *path, GError **error)
+{
+    nm_auto_unref_keyfile GKeyFile *keyfile  = NULL;
+    gs_free char                   *base_dir = NULL;
+    char                           *sep;
+
+    keyfile = g_key_file_new();
+    if (!g_key_file_load_from_file(keyfile, path, G_KEY_FILE_NONE, error))
+        return NULL;
+
+    sep      = strrchr(path, '/');
+    base_dir = g_strndup(path, sep - path);
+
+    return nm_keyfile_read(keyfile, base_dir, NM_KEYFILE_HANDLER_FLAGS_NONE, NULL, NULL, error);
+}
+
 static NMSettingsConnection *
-find_settings_connection(NMCheckpoint     *self,
-                         DeviceCheckpoint *dev_checkpoint,
-                         gboolean         *need_update,
-                         gboolean         *need_activation)
+find_settings_connection(NMCheckpoint                    *self,
+                         DeviceCheckpoint                *dev_checkpoint,
+                         gboolean                        *need_update,
+                         gboolean                        *need_update_shadowed,
+                         gboolean                        *need_activation,
+                         NMSettingsConnectionPersistMode *persist_mode)
 {
     NMCheckpointPrivate  *priv = NM_CHECKPOINT_GET_PRIVATE(self);
     NMActiveConnection   *active;
     NMSettingsConnection *sett_conn;
+    const char           *shadowed_file;
+    NMConnection         *shadowed_connection = NULL;
     const char           *uuid, *ac_uuid;
     const CList          *tmp_clist;
-
-    *need_activation = FALSE;
-    *need_update     = FALSE;
+    gboolean              sett_conn_unsaved;
+    NMSettingsStorage    *storage;
+
+    *need_activation      = FALSE;
+    *need_update          = FALSE;
+    *need_update_shadowed = FALSE;
+
+    /* With regard to storage, there are 4 different possible states for the settings
+     * connection: 1) persistent; 2) in-memory only; 3) in-memory shadowing a persistent
+     * file; 4) in-memory shadowing a detached persistent file (i.e. the deletion of
+     * the connection doesn't delete the persistent file).
+     */
+    if (dev_checkpoint->settings_connection_is_unsaved) {
+        if (dev_checkpoint->settings_connection_shadowed) {
+            if (dev_checkpoint->settings_connection_is_shadowed_owned)
+                *persist_mode = NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY;
+            else
+                *persist_mode = NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY_DETACHED;
+        } else
+            *persist_mode = NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY_ONLY;
+    } else {
+        *persist_mode = NM_SETTINGS_CONNECTION_PERSIST_MODE_TO_DISK;
+    }
 
     uuid      = nm_connection_get_uuid(dev_checkpoint->settings_connection);
     sett_conn = nm_settings_get_connection_by_uuid(NM_SETTINGS_GET, uuid);
 
-    if (!sett_conn)
-        return NULL;
-
-    /* Now check if the connection changed, ... */
-    if (!nm_connection_compare(dev_checkpoint->settings_connection,
-                               nm_settings_connection_get_connection(sett_conn),
-                               NM_SETTING_COMPARE_FLAG_EXACT)) {
+    /* Check if the connection changed */
+    if (sett_conn
+        && !nm_connection_compare(dev_checkpoint->settings_connection,
+                                  nm_settings_connection_get_connection(sett_conn),
+                                  NM_SETTING_COMPARE_FLAG_IGNORE_TIMESTAMP)) {
         _LOGT("rollback: settings connection %s changed", uuid);
         *need_update     = TRUE;
         *need_activation = TRUE;
     }
 
-    /* ... is active, ... */
+    storage       = sett_conn ? nm_settings_connection_get_storage(sett_conn) : NULL;
+    shadowed_file = storage ? nm_settings_storage_get_shadowed_storage(storage, NULL) : NULL;
+    shadowed_connection =
+        shadowed_file ? parse_connection_from_shadowed_file(shadowed_file, NULL) : NULL;
+
+    if (dev_checkpoint->settings_connection_shadowed) {
+        if (!shadowed_connection
+            || !nm_connection_compare(dev_checkpoint->settings_connection_shadowed,
+                                      shadowed_connection,
+                                      NM_SETTING_COMPARE_FLAG_IGNORE_TIMESTAMP)) {
+            _LOGT("rollback: shadowed connection changed for %s", uuid);
+            *need_update_shadowed = TRUE;
+            *need_update          = TRUE;
+        }
+    } else {
+        if (shadowed_connection) {
+            _LOGT("rollback: shadowed connection changed for %s", uuid);
+            *need_update = TRUE;
+        }
+    }
+
+    if (!sett_conn)
+        return NULL;
+
+    /* Check if the connection unsaved flag changed */
+    sett_conn_unsaved = NM_FLAGS_HAS(nm_settings_connection_get_flags(sett_conn),
+                                     NM_SETTINGS_CONNECTION_INT_FLAGS_UNSAVED);
+    if (sett_conn_unsaved != dev_checkpoint->settings_connection_is_unsaved) {
+        _LOGT("rollback: storage changed for settings connection %s: unsaved (%d -> %d)",
+              uuid,
+              dev_checkpoint->settings_connection_is_unsaved,
+              sett_conn_unsaved);
+        *need_update = TRUE;
+    }
+
+    /* Check if the active state changed */
     nm_manager_for_each_active_connection (priv->manager, active, tmp_clist) {
         ac_uuid =
             nm_settings_connection_get_uuid(nm_active_connection_get_settings_connection(active));
@@ -193,7 +275,7 @@ find_settings_connection(NMCheckpoint     *self,
         return sett_conn;
     }
 
-    /* ... or if the connection was reactivated/reapplied */
+    /* Check if the connection was reactivated/reapplied */
     if (nm_active_connection_version_id_get(active) != dev_checkpoint->ac_version_id) {
         _LOGT("rollback: active connection version id of %s changed", uuid);
         *need_activation = TRUE;
@@ -209,12 +291,19 @@ restore_and_activate_connection(NMCheckpoint *self, DeviceCheckpoint *dev_checkp
     NMSettingsConnection           *connection;
     gs_unref_object NMAuthSubject  *subject     = NULL;
     GError                         *local_error = NULL;
-    gboolean                        need_update, need_activation;
+    gboolean                        need_update;
+    gboolean                        need_update_shadowed;
+    gboolean                        need_activation;
     NMSettingsConnectionPersistMode persist_mode;
     NMSettingsConnectionIntFlags    sett_flags;
     NMSettingsConnectionIntFlags    sett_mask;
 
-    connection = find_settings_connection(self, dev_checkpoint, &need_update, &need_activation);
+    connection = find_settings_connection(self,
+                                          dev_checkpoint,
+                                          &need_update,
+                                          &need_update_shadowed,
+                                          &need_activation,
+                                          &persist_mode);
 
     /* FIXME: we need to ensure to re-create/update the profile for the
      *   same settings plugin. E.g. if it was a keyfile in /run or /etc,
@@ -226,9 +315,26 @@ restore_and_activate_connection(NMCheckpoint *self, DeviceCheckpoint *dev_checkp
     sett_mask  = NM_SETTINGS_CONNECTION_INT_FLAGS_NONE;
 
     if (connection) {
+        if (need_update_shadowed) {
+            _LOGD("rollback: updating shadowed file for connection %s",
+                  nm_connection_get_uuid(dev_checkpoint->settings_connection));
+            nm_settings_connection_update(
+                connection,
+                NULL,
+                dev_checkpoint->settings_connection_shadowed,
+                NM_SETTINGS_CONNECTION_PERSIST_MODE_TO_DISK,
+                sett_flags,
+                sett_mask,
+                NM_SETTINGS_CONNECTION_UPDATE_REASON_RESET_SYSTEM_SECRETS
+                    | NM_SETTINGS_CONNECTION_UPDATE_REASON_UPDATE_NON_SECRET,
+                "checkpoint-rollback",
+                NULL);
+        }
+
         if (need_update) {
-            _LOGD("rollback: updating connection %s", nm_settings_connection_get_uuid(connection));
-            persist_mode = NM_SETTINGS_CONNECTION_PERSIST_MODE_KEEP;
+            _LOGD("rollback: updating connection %s with persist mode \"%s\"",
+                  nm_connection_get_uuid(dev_checkpoint->settings_connection),
+                  nm_settings_connection_persist_mode_to_string(persist_mode));
             nm_settings_connection_update(
                 connection,
                 NULL,
@@ -243,21 +349,54 @@ restore_and_activate_connection(NMCheckpoint *self, DeviceCheckpoint *dev_checkp
         }
     } else {
         /* The connection was deleted, recreate it */
-        _LOGD("rollback: adding connection %s again",
-              nm_connection_get_uuid(dev_checkpoint->settings_connection));
-
-        persist_mode = NM_SETTINGS_CONNECTION_PERSIST_MODE_TO_DISK;
-        if (!nm_settings_add_connection(NM_SETTINGS_GET,
-                                        NULL,
-                                        dev_checkpoint->settings_connection,
-                                        persist_mode,
-                                        NM_SETTINGS_CONNECTION_ADD_REASON_NONE,
-                                        sett_flags,
-                                        &connection,
-                                        &local_error)) {
-            _LOGD("rollback: connection add failure: %s", local_error->message);
-            g_clear_error(&local_error);
-            return FALSE;
+        if (need_update_shadowed) {
+            _LOGD("rollback: adding back shadowed file for connection %s",
+                  nm_connection_get_uuid(dev_checkpoint->settings_connection));
+
+            if (!nm_settings_add_connection(NM_SETTINGS_GET,
+                                            NULL,
+                                            dev_checkpoint->settings_connection_shadowed,
+                                            NM_SETTINGS_CONNECTION_PERSIST_MODE_TO_DISK,
+                                            NM_SETTINGS_CONNECTION_ADD_REASON_NONE,
+                                            sett_flags,
+                                            &connection,
+                                            &local_error)) {
+                _LOGD("rollback: connection add failure: %s", local_error->message);
+                g_clear_error(&local_error);
+                return FALSE;
+            }
+
+            _LOGD("rollback: updating connection %s with persist mode \"%s\"",
+                  nm_connection_get_uuid(dev_checkpoint->settings_connection),
+                  nm_settings_connection_persist_mode_to_string(persist_mode));
+
+            nm_settings_connection_update(
+                connection,
+                NULL,
+                dev_checkpoint->settings_connection,
+                persist_mode,
+                sett_flags,
+                sett_mask,
+                NM_SETTINGS_CONNECTION_UPDATE_REASON_RESET_SYSTEM_SECRETS
+                    | NM_SETTINGS_CONNECTION_UPDATE_REASON_UPDATE_NON_SECRET,
+                "checkpoint-rollback",
+                NULL);
+        } else {
+            _LOGD("rollback: adding back connection %s with persist mode \"%s\"",
+                  nm_connection_get_uuid(dev_checkpoint->settings_connection),
+                  nm_settings_connection_persist_mode_to_string(persist_mode));
+            if (!nm_settings_add_connection(NM_SETTINGS_GET,
+                                            NULL,
+                                            dev_checkpoint->settings_connection,
+                                            persist_mode,
+                                            NM_SETTINGS_CONNECTION_ADD_REASON_NONE,
+                                            sett_flags,
+                                            &connection,
+                                            &local_error)) {
+                _LOGD("rollback: connection add failure: %s", local_error->message);
+                g_clear_error(&local_error);
+                return FALSE;
+            }
         }
         need_activation = TRUE;
     }
@@ -359,11 +498,15 @@ nm_checkpoint_rollback(NMCheckpoint *self)
     while (g_hash_table_iter_next(&iter, (gpointer *) &device, (gpointer *) &dev_checkpoint)) {
         guint32 result = NM_ROLLBACK_RESULT_OK;
 
-        _LOGD("rollback: restoring device %s (state %d, realized %d, explicitly unmanaged %d)",
+        _LOGD("rollback: restoring device %s (state %d, realized %d, explicitly unmanaged %d, "
+              "connection-unsaved %d, connection-shadowed %d, connection-shadowed-owned %d)",
               dev_checkpoint->original_dev_name,
               (int) dev_checkpoint->state,
               dev_checkpoint->realized,
-              dev_checkpoint->unmanaged_explicit);
+              dev_checkpoint->unmanaged_explicit,
+              dev_checkpoint->settings_connection_is_unsaved,
+              !!dev_checkpoint->settings_connection_shadowed,
+              dev_checkpoint->settings_connection_is_shadowed_owned);
 
         if (nm_device_is_real(device)) {
             if (!dev_checkpoint->realized) {
@@ -491,6 +634,17 @@ next_dev:
             }
         }
     }
+    if (NM_FLAGS_HAS(priv->flags, NM_CHECKPOINT_CREATE_FLAG_TRACK_INTERNAL_GLOBAL_DNS)
+        && priv->global_dns_config) {
+        gs_free_error GError *error = NULL;
+        NMConfig             *config;
+
+        config = nm_manager_get_config(priv->manager);
+        nm_assert(config);
+        if (!nm_config_set_global_dns(config, priv->global_dns_config, &error)) {
+            _LOGE("set global DNS failed with error: %s", error->message);
+        }
+    }
 
     return g_variant_new("(a{su})", &builder);
 }
@@ -504,6 +658,7 @@ device_checkpoint_destroy(gpointer data)
     g_clear_object(&dev_checkpoint->applied_connection);
     g_clear_object(&dev_checkpoint->settings_connection);
     g_clear_object(&dev_checkpoint->device);
+    g_clear_object(&dev_checkpoint->settings_connection_shadowed);
     g_free(dev_checkpoint->original_dev_path);
     g_free(dev_checkpoint->original_dev_name);
 
@@ -541,7 +696,7 @@ _dev_exported_changed(NMDBusObject *obj, NMCheckpoint *checkpoint)
 }
 
 static DeviceCheckpoint *
-device_checkpoint_create(NMCheckpoint *checkpoint, NMDevice *device)
+device_checkpoint_create(NMCheckpoint *self, NMDevice *device)
 {
     DeviceCheckpoint     *dev_checkpoint;
     NMConnection         *applied_connection;
@@ -565,7 +720,7 @@ device_checkpoint_create(NMCheckpoint *checkpoint, NMDevice *device)
     dev_checkpoint->dev_exported_change_id = g_signal_connect(device,
                                                               NM_DBUS_OBJECT_EXPORTED_CHANGED,
                                                               G_CALLBACK(_dev_exported_changed),
-                                                              checkpoint);
+                                                              self);
 
     if (nm_device_get_unmanaged_mask(device, NM_UNMANAGED_USER_EXPLICIT)) {
         dev_checkpoint->unmanaged_explicit =
@@ -575,6 +730,11 @@ device_checkpoint_create(NMCheckpoint *checkpoint, NMDevice *device)
 
     act_request = nm_device_get_act_request(device);
     if (act_request) {
+        NMSettingsStorage    *storage;
+        gboolean              shadowed_owned = FALSE;
+        const char           *shadowed_file;
+        gs_free_error GError *error = NULL;
+
         settings_connection = nm_act_request_get_settings_connection(act_request);
         applied_connection  = nm_act_request_get_applied_connection(act_request);
 
@@ -588,6 +748,24 @@ device_checkpoint_create(NMCheckpoint *checkpoint, NMDevice *device)
         dev_checkpoint->activation_lifetime_bound_to_profile_visibility =
             NM_FLAGS_HAS(nm_active_connection_get_state_flags(NM_ACTIVE_CONNECTION(act_request)),
                          NM_ACTIVATION_STATE_FLAG_LIFETIME_BOUND_TO_PROFILE_VISIBILITY);
+
+        dev_checkpoint->settings_connection_is_unsaved =
+            NM_FLAGS_HAS(nm_settings_connection_get_flags(settings_connection),
+                         NM_SETTINGS_CONNECTION_INT_FLAGS_UNSAVED);
+
+        storage = nm_settings_connection_get_storage(settings_connection);
+        shadowed_file =
+            storage ? nm_settings_storage_get_shadowed_storage(storage, &shadowed_owned) : NULL;
+        if (shadowed_file) {
+            dev_checkpoint->settings_connection_is_shadowed_owned = shadowed_owned;
+            dev_checkpoint->settings_connection_shadowed =
+                parse_connection_from_shadowed_file(shadowed_file, &error);
+            if (!dev_checkpoint->settings_connection_shadowed) {
+                _LOGW("error reading shadowed connection file for %s: %s",
+                      nm_device_get_iface(device),
+                      error->message);
+            }
+        }
     }
 
     return dev_checkpoint;
@@ -742,6 +920,19 @@ nm_checkpoint_new(NMManager              *manager,
                                             NM_MANAGER_DEVICE_REMOVED,
                                             G_CALLBACK(_device_removed),
                                             self);
+    if (NM_FLAGS_HAS(flags, NM_CHECKPOINT_CREATE_FLAG_TRACK_INTERNAL_GLOBAL_DNS)) {
+        NMConfigData      *config_data;
+        NMGlobalDnsConfig *dns_config = NULL;
+
+        config_data = nm_config_get_data(nm_manager_get_config(manager));
+        if (config_data) {
+            dns_config = nm_config_data_get_global_dns_config(config_data);
+            if (!dns_config || nm_global_dns_config_is_internal(dns_config)) {
+                priv->global_dns_config = nm_global_dns_config_clone(dns_config);
+            }
+        }
+    }
+
     return self;
 }
 
@@ -756,6 +947,7 @@ dispose(GObject *object)
     nm_clear_pointer(&priv->devices, g_hash_table_unref);
     nm_clear_pointer(&priv->connection_uuids, g_hash_table_unref);
     nm_clear_pointer(&priv->removed_devices, g_ptr_array_unref);
+    nm_global_dns_config_free(priv->global_dns_config);
 
     nm_clear_g_signal_handler(priv->manager, &priv->dev_removed_id);
     g_clear_object(&priv->manager);
diff --git a/src/core/nm-config-data.c b/src/core/nm-config-data.c
index 468e56b8..d4498edd 100644
--- a/src/core/nm-config-data.c
+++ b/src/core/nm-config-data.c
@@ -2436,3 +2436,50 @@ nm_config_data_class_init(NMConfigDataClass *config_class)
 
     g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
 }
+
+static NMGlobalDnsDomain *
+nm_global_dns_domain_clone(NMGlobalDnsDomain *old_domain)
+{
+    if (old_domain) {
+        NMGlobalDnsDomain *new_domain = g_malloc0(sizeof(NMGlobalDnsDomain));
+        new_domain->name              = g_strdup(old_domain->name);
+        new_domain->servers           = (char **) nm_strv_dup(old_domain->servers, -1, TRUE);
+        new_domain->options           = (char **) nm_strv_dup(old_domain->options, -1, TRUE);
+        return new_domain;
+    } else {
+        return NULL;
+    }
+}
+
+NMGlobalDnsConfig *
+nm_global_dns_config_clone(NMGlobalDnsConfig *old_dns_config)
+{
+    NMGlobalDnsConfig *new_dns_config;
+    gpointer           key, value;
+    NMGlobalDnsDomain *old_domain;
+    GHashTableIter     iter;
+
+    new_dns_config           = g_malloc0(sizeof(NMGlobalDnsConfig));
+    new_dns_config->internal = TRUE;
+
+    if (old_dns_config) {
+        new_dns_config->internal = old_dns_config->internal;
+        new_dns_config->searches = nm_strv_dup(old_dns_config->searches, -1, TRUE);
+        new_dns_config->options  = nm_strv_dup(old_dns_config->options, -1, TRUE);
+        new_dns_config->domains  = g_hash_table_new_full(nm_str_hash,
+                                                        g_str_equal,
+                                                        g_free,
+                                                        (GDestroyNotify) global_dns_domain_free);
+        if (old_dns_config->domains) {
+            g_hash_table_iter_init(&iter, old_dns_config->domains);
+            while (g_hash_table_iter_next(&iter, &key, &value)) {
+                old_domain = value;
+                g_hash_table_insert(new_dns_config->domains,
+                                    g_strdup(key),
+                                    nm_global_dns_domain_clone(old_domain));
+            }
+        }
+        global_dns_config_seal_domains(new_dns_config);
+    }
+    return new_dns_config;
+}
diff --git a/src/core/nm-config-data.h b/src/core/nm-config-data.h
index 9e7a50fc..0344ce90 100644
--- a/src/core/nm-config-data.h
+++ b/src/core/nm-config-data.h
@@ -280,7 +280,8 @@ int                nm_global_dns_config_cmp(const NMGlobalDnsConfig *a,
                                             const NMGlobalDnsConfig *b,
                                             gboolean                 check_internal);
 void nm_global_dns_config_update_checksum(const NMGlobalDnsConfig *dns_config, GChecksum *sum);
-void nm_global_dns_config_free(NMGlobalDnsConfig *dns_config);
+NMGlobalDnsConfig *nm_global_dns_config_clone(NMGlobalDnsConfig *dns_config);
+void               nm_global_dns_config_free(NMGlobalDnsConfig *dns_config);
 
 NMGlobalDnsConfig *nm_global_dns_config_from_dbus(const GValue *value, GError **error);
 void               nm_global_dns_config_to_dbus(const NMGlobalDnsConfig *dns_config, GValue *value);
diff --git a/src/core/nm-connectivity.h b/src/core/nm-connectivity.h
index d9ea95c2..e0353f9f 100644
--- a/src/core/nm-connectivity.h
+++ b/src/core/nm-connectivity.h
@@ -25,10 +25,10 @@ nm_connectivity_state_cmp(NMConnectivityState a, NMConnectivityState b)
 
 /*****************************************************************************/
 
-#define NM_CONNECTIVITY_ERROR     ((NMConnectivityState) -1)
-#define NM_CONNECTIVITY_FAKE      ((NMConnectivityState) -2)
-#define NM_CONNECTIVITY_CANCELLED ((NMConnectivityState) -3)
-#define NM_CONNECTIVITY_DISPOSING ((NMConnectivityState) -4)
+#define NM_CONNECTIVITY_ERROR     ((NMConnectivityState) - 1)
+#define NM_CONNECTIVITY_FAKE      ((NMConnectivityState) - 2)
+#define NM_CONNECTIVITY_CANCELLED ((NMConnectivityState) - 3)
+#define NM_CONNECTIVITY_DISPOSING ((NMConnectivityState) - 4)
 
 #define NM_TYPE_CONNECTIVITY (nm_connectivity_get_type())
 #define NM_CONNECTIVITY(obj) \
diff --git a/src/core/nm-core-utils.c b/src/core/nm-core-utils.c
index 178ea3c4..dd1e9939 100644
--- a/src/core/nm-core-utils.c
+++ b/src/core/nm-core-utils.c
@@ -121,7 +121,8 @@ _nm_singleton_instance_weak_cb(gpointer data, GObject *where_the_object_was)
     _singletons = g_slist_remove(_singletons, where_the_object_was);
 }
 
-static void __attribute__((destructor)) _nm_singleton_instance_destroy(void)
+static void __attribute__((destructor))
+_nm_singleton_instance_destroy(void)
 {
     _singletons_shutdown = TRUE;
 
diff --git a/src/core/nm-core-utils.h b/src/core/nm-core-utils.h
index f015236d..fdfed5f6 100644
--- a/src/core/nm-core-utils.h
+++ b/src/core/nm-core-utils.h
@@ -306,7 +306,7 @@ typedef enum {
     NM_UTILS_STABLE_TYPE_RANDOM    = 3,
 } NMUtilsStableType;
 
-#define NM_UTILS_STABLE_TYPE_NONE ((NMUtilsStableType) -1)
+#define NM_UTILS_STABLE_TYPE_NONE ((NMUtilsStableType) - 1)
 
 NMUtilsStableType nm_utils_stable_id_parse(const char *stable_id,
                                            const char *deviceid,
diff --git a/src/core/nm-l3cfg.c b/src/core/nm-l3cfg.c
index f428d04c..12357fba 100644
--- a/src/core/nm-l3cfg.c
+++ b/src/core/nm-l3cfg.c
@@ -4338,7 +4338,7 @@ _l3_commit_ndisc_params(NML3Cfg *self, NML3CfgCommitType commit_type)
     if (l3cd) {
         reachable_set = nm_l3_config_data_get_ndisc_reachable_time_msec(l3cd, &reachable);
         retrans_set   = nm_l3_config_data_get_ndisc_retrans_timer_msec(l3cd, &retrans);
-        hop_limit     = nm_l3_config_data_get_ndisc_hop_limit(l3cd, &hop_limit);
+        hop_limit_set = nm_l3_config_data_get_ndisc_hop_limit(l3cd, &hop_limit);
     }
     ifname = nm_l3cfg_get_ifname(self, TRUE);
 
diff --git a/src/core/nm-manager.c b/src/core/nm-manager.c
index 730ba476..b2a827e3 100644
--- a/src/core/nm-manager.c
+++ b/src/core/nm-manager.c
@@ -46,7 +46,7 @@
 #include "nm-priv-helper-call.h"
 #include "nm-rfkill-manager.h"
 #include "nm-session-monitor.h"
-#include "nm-sleep-monitor.h"
+#include "nm-power-monitor.h"
 #include "settings/nm-settings-connection.h"
 #include "settings/nm-settings.h"
 #include "vpn/nm-vpn-manager.h"
@@ -214,7 +214,7 @@ typedef struct {
 
     NMVpnManager *vpn_manager;
 
-    NMSleepMonitor *sleep_monitor;
+    NMPowerMonitor *power_monitor;
 
     NMAuthManager *auth_mgr;
 
@@ -1914,7 +1914,8 @@ find_device_by_ip_iface(NMManager *self, const char *iface)
  * is given, this function will only return master devices and will ensure
  * @slave, when activated, can be a slave of the returned master device.  If
  * @connection is given, this function will only consider devices that are
- * compatible with @connection.
+ * compatible with @connection. If @child is given, this function will only
+ * return parent device.
  *
  * Returns: the matching #NMDevice
  */
@@ -1922,7 +1923,8 @@ static NMDevice *
 find_device_by_iface(NMManager    *self,
                      const char   *iface,
                      NMConnection *connection,
-                     NMConnection *slave)
+                     NMConnection *slave,
+                     NMConnection *child)
 {
     NMManagerPrivate *priv     = NM_MANAGER_GET_PRIVATE(self);
     NMDevice         *fallback = NULL;
@@ -1936,11 +1938,13 @@ find_device_by_iface(NMManager    *self,
         if (connection && !nm_device_check_connection_compatible(candidate, connection, TRUE, NULL))
             continue;
         if (slave) {
-            if (!nm_device_is_master(candidate))
+            if (!nm_device_is_controller(candidate))
                 continue;
             if (!nm_device_check_slave_connection_compatible(candidate, slave))
                 continue;
         }
+        if (child && !nm_device_can_be_parent(candidate))
+            continue;
 
         if (nm_device_is_real(candidate))
             return candidate;
@@ -2405,7 +2409,7 @@ find_parent_device_for_connection(NMManager       *self,
     NM_SET_OUT(out_parent_spec, parent_name);
 
     /* Try as an interface name of a parent device */
-    parent = find_device_by_iface(self, parent_name, NULL, NULL);
+    parent = find_device_by_iface(self, parent_name, NULL, NULL, connection);
     if (parent)
         return parent;
 
@@ -3777,7 +3781,7 @@ recheck_assume_connection(NMManager *self, NMDevice *device)
                         &master_ac,
                         NULL)
             && master_ac)
-            nm_active_connection_set_master(active, master_ac);
+            nm_active_connection_set_controller(active, master_ac);
 
         active_connection_add(self, active);
         nm_device_queue_activation(device, NM_ACT_REQUEST(active));
@@ -4434,10 +4438,25 @@ platform_query_devices(NMManager *self)
     links        = nm_platform_link_get_all(priv->platform);
     if (!links)
         return;
+
     for (i = 0; i < links->len; i++) {
-        const NMPlatformLink          *link = NMP_OBJECT_CAST_LINK(links->pdata[i]);
+        const NMPlatformLink          *elem = NMP_OBJECT_CAST_LINK(links->pdata[i]);
+        const NMPlatformLink          *link;
         const NMConfigDeviceStateData *dev_state;
 
+        /*
+         * @links is an immutable snapshot of the platform links captured before
+         * the loop was started. It's possible that in the meantime, while
+         * processing netlink events in platform_link_added(), a link was
+         * renamed.  If that happens, we have 2 different views of the same
+         * ifindex: the one from @links and the one from platform. This can
+         * cause race conditions; make sure to use the latest known version of
+         * the link.
+         */
+        link = nm_platform_link_get(priv->platform, elem->ifindex);
+        if (!link)
+            continue;
+
         dev_state = nm_config_device_state_get(priv->config, link->ifindex);
         platform_link_added(self,
                             link->ifindex,
@@ -4921,7 +4940,7 @@ find_master(NMManager             *self,
     nm_assert(!out_master_ac || !*out_master_ac);
 
     s_con  = nm_connection_get_setting_connection(connection);
-    master = nm_setting_connection_get_master(s_con);
+    master = nm_setting_connection_get_controller(s_con);
 
     if (master == NULL)
         return TRUE; /* success, but no master */
@@ -5003,7 +5022,7 @@ find_master(NMManager             *self,
     }
 
     if (!master_connection) {
-        master_device = find_device_by_iface(self, master, NULL, connection);
+        master_device = find_device_by_iface(self, master, NULL, connection, NULL);
         if (!master_device) {
             g_set_error(error,
                         NM_MANAGER_ERROR,
@@ -5924,7 +5943,7 @@ _internal_activate_device(NMManager *self, NMActiveConnection *active, GError **
                                              NM_DEVICE_STATE_REASON_USER_REQUESTED);
         }
 
-        nm_active_connection_set_master(active, master_ac);
+        nm_active_connection_set_controller(active, master_ac);
         _LOGD(LOGD_CORE,
               "Activation of '%s' depends on active connection %p %s",
               nm_settings_connection_get_id(sett_conn),
@@ -6445,7 +6464,7 @@ validate_activation_request(NMManager             *self,
             if (!iface)
                 return NULL;
 
-            device = find_device_by_iface(self, iface, connection, NULL);
+            device = find_device_by_iface(self, iface, connection, NULL, NULL);
             if (!device) {
                 g_set_error_literal(error,
                                     NM_MANAGER_ERROR,
@@ -7128,7 +7147,7 @@ static gboolean
 sleep_devices_add(NMManager *self, NMDevice *device, gboolean suspending)
 {
     NMManagerPrivate              *priv   = NM_MANAGER_GET_PRIVATE(self);
-    NMSleepMonitorInhibitorHandle *handle = NULL;
+    NMPowerMonitorInhibitorHandle *handle = NULL;
 
     if (g_hash_table_lookup_extended(priv->sleep_devices, device, NULL, (gpointer *) &handle)) {
         if (suspending) {
@@ -7136,16 +7155,16 @@ sleep_devices_add(NMManager *self, NMDevice *device, gboolean suspending)
              * Even if we had an old handle, it might be stale by now. */
             g_hash_table_insert(priv->sleep_devices,
                                 device,
-                                nm_sleep_monitor_inhibit_take(priv->sleep_monitor));
+                                nm_power_monitor_inhibit_take(priv->power_monitor));
             if (handle)
-                nm_sleep_monitor_inhibit_release(priv->sleep_monitor, handle);
+                nm_power_monitor_inhibit_release(priv->power_monitor, handle);
         }
         return FALSE;
     }
 
     g_hash_table_insert(priv->sleep_devices,
                         g_object_ref(device),
-                        suspending ? nm_sleep_monitor_inhibit_take(priv->sleep_monitor) : NULL);
+                        suspending ? nm_power_monitor_inhibit_take(priv->power_monitor) : NULL);
     g_signal_connect(device, "notify::" NM_DEVICE_STATE, G_CALLBACK(device_sleep_cb), self);
     return TRUE;
 }
@@ -7154,13 +7173,13 @@ static gboolean
 sleep_devices_remove(NMManager *self, NMDevice *device)
 {
     NMManagerPrivate              *priv = NM_MANAGER_GET_PRIVATE(self);
-    NMSleepMonitorInhibitorHandle *handle;
+    NMPowerMonitorInhibitorHandle *handle;
 
     if (!g_hash_table_lookup_extended(priv->sleep_devices, device, NULL, (gpointer *) &handle))
         return FALSE;
 
     if (handle)
-        nm_sleep_monitor_inhibit_release(priv->sleep_monitor, handle);
+        nm_power_monitor_inhibit_release(priv->power_monitor, handle);
 
     /* Remove device from hash */
     g_signal_handlers_disconnect_by_func(device, device_sleep_cb, self);
@@ -7177,14 +7196,14 @@ sleep_devices_clear(NMManager *self)
 {
     NMManagerPrivate              *priv = NM_MANAGER_GET_PRIVATE(self);
     NMDevice                      *device;
-    NMSleepMonitorInhibitorHandle *handle;
+    NMPowerMonitorInhibitorHandle *handle;
     GHashTableIter                 iter;
 
     g_hash_table_iter_init(&iter, priv->sleep_devices);
     while (g_hash_table_iter_next(&iter, (gpointer *) &device, (gpointer *) &handle)) {
         g_signal_handlers_disconnect_by_func(device, device_sleep_cb, self);
         if (handle)
-            nm_sleep_monitor_inhibit_release(priv->sleep_monitor, handle);
+            nm_power_monitor_inhibit_release(priv->power_monitor, handle);
         g_object_unref(device);
         g_hash_table_iter_remove(&iter);
     }
@@ -7216,6 +7235,33 @@ device_sleep_cb(NMDevice *device, GParamSpec *pspec, NMManager *self)
 }
 
 static void
+_handle_device_takedown(NMManager *self,
+                        NMDevice  *device,
+                        gboolean   suspending,
+                        gboolean   is_shutdown)
+{
+    nm_device_notify_sleeping(device);
+
+    if (nm_device_is_activating(device)
+        || nm_device_get_state(device) == NM_DEVICE_STATE_ACTIVATED) {
+        _LOGD(LOGD_SUSPEND,
+              "%s: wait disconnection of device %s",
+              is_shutdown ? "shutdown" : "sleep",
+              nm_device_get_ip_iface(device));
+
+        if (sleep_devices_add(self, device, suspending))
+            nm_device_queue_state(device,
+                                  NM_DEVICE_STATE_DEACTIVATING,
+                                  NM_DEVICE_STATE_REASON_SLEEPING);
+    } else {
+        nm_device_set_unmanaged_by_flags(device,
+                                         NM_UNMANAGED_SLEEPING,
+                                         NM_UNMAN_FLAG_OP_SET_UNMANAGED,
+                                         NM_DEVICE_STATE_REASON_SLEEPING);
+    }
+}
+
+static void
 do_sleep_wake(NMManager *self, gboolean sleeping_changed)
 {
     NMManagerPrivate *priv = NM_MANAGER_GET_PRIVATE(self);
@@ -7249,24 +7295,7 @@ do_sleep_wake(NMManager *self, gboolean sleeping_changed)
                 continue;
             }
 
-            nm_device_notify_sleeping(device);
-
-            if (nm_device_is_activating(device)
-                || nm_device_get_state(device) == NM_DEVICE_STATE_ACTIVATED) {
-                _LOGD(LOGD_SUSPEND,
-                      "sleep: wait disconnection of device %s",
-                      nm_device_get_ip_iface(device));
-
-                if (sleep_devices_add(self, device, suspending))
-                    nm_device_queue_state(device,
-                                          NM_DEVICE_STATE_DEACTIVATING,
-                                          NM_DEVICE_STATE_REASON_SLEEPING);
-            } else {
-                nm_device_set_unmanaged_by_flags(device,
-                                                 NM_UNMANAGED_SLEEPING,
-                                                 NM_UNMAN_FLAG_OP_SET_UNMANAGED,
-                                                 NM_DEVICE_STATE_REASON_SLEEPING);
-            }
+            _handle_device_takedown(self, device, suspending, FALSE);
         }
     } else {
         _LOGD(LOGD_SUSPEND, "sleep: %s...", waking_from_suspend ? "waking up" : "re-enabling");
@@ -7438,7 +7467,7 @@ impl_manager_sleep(NMDBusObject                      *obj,
 }
 
 static void
-sleeping_cb(NMSleepMonitor *monitor, gboolean is_about_to_suspend, gpointer user_data)
+sleeping_cb(NMPowerMonitor *monitor, gboolean is_about_to_suspend, gpointer user_data)
 {
     NMManager *self = user_data;
 
@@ -7447,6 +7476,41 @@ sleeping_cb(NMSleepMonitor *monitor, gboolean is_about_to_suspend, gpointer user
 }
 
 static void
+shutdown_cb(NMPowerMonitor *monitor, gpointer user_data)
+{
+    NMManager        *self = user_data;
+    NMManagerPrivate *priv = NM_MANAGER_GET_PRIVATE(self);
+    NMDevice         *device;
+
+    _LOGT(LOGD_SUSPEND, "shutdown: received shutdown signal");
+
+    c_list_for_each_entry (device, &priv->devices_lst_head, devices_lst) {
+        NMSettingConnection *s_con;
+        gboolean             take_down = FALSE;
+
+        s_con = nm_device_get_applied_setting(device, NM_TYPE_SETTING_CONNECTION);
+        if (!s_con)
+            continue;
+
+        if (nm_setting_connection_get_down_on_poweroff(s_con)
+            == NM_SETTING_CONNECTION_DOWN_ON_POWEROFF_YES)
+            take_down = TRUE;
+        else if (nm_setting_connection_get_down_on_poweroff(s_con)
+                 == NM_SETTING_CONNECTION_DOWN_ON_POWEROFF_DEFAULT)
+            take_down = nm_config_data_get_connection_default_int64(
+                NM_CONFIG_GET_DATA,
+                NM_CON_DEFAULT("connection.down-on-poweroff"),
+                device,
+                NM_SETTING_CONNECTION_DOWN_ON_POWEROFF_NO,
+                NM_SETTING_CONNECTION_DOWN_ON_POWEROFF_YES,
+                NM_SETTING_CONNECTION_DOWN_ON_POWEROFF_NO);
+
+        if (take_down)
+            _handle_device_takedown(self, device, FALSE, TRUE);
+    }
+}
+
+static void
 _internal_enable(NMManager *self, gboolean enable)
 {
     NMManagerPrivate *priv = NM_MANAGER_GET_PRIVATE(self);
@@ -8458,7 +8522,8 @@ impl_manager_checkpoint_create(NMDBusObject                      *obj,
                                      | NM_CHECKPOINT_CREATE_FLAG_DELETE_NEW_CONNECTIONS
                                      | NM_CHECKPOINT_CREATE_FLAG_DISCONNECT_NEW_DEVICES
                                      | NM_CHECKPOINT_CREATE_FLAG_ALLOW_OVERLAPPING
-                                     | NM_CHECKPOINT_CREATE_FLAG_NO_PRESERVE_EXTERNAL_PORTS)))) {
+                                     | NM_CHECKPOINT_CREATE_FLAG_NO_PRESERVE_EXTERNAL_PORTS
+                                     | NM_CHECKPOINT_CREATE_FLAG_TRACK_INTERNAL_GLOBAL_DNS)))) {
         g_dbus_method_invocation_return_error_literal(invocation,
                                                       NM_MANAGER_ERROR,
                                                       NM_MANAGER_ERROR_INVALID_ARGUMENTS,
@@ -8834,8 +8899,9 @@ nm_manager_init(NMManager *self)
     priv->devcon_data_dict = g_hash_table_new(_devcon_data_hash, _devcon_data_equal);
 
     /* sleep/wake handling */
-    priv->sleep_monitor = nm_sleep_monitor_new();
-    g_signal_connect(priv->sleep_monitor, NM_SLEEP_MONITOR_SLEEPING, G_CALLBACK(sleeping_cb), self);
+    priv->power_monitor = nm_power_monitor_new();
+    g_signal_connect(priv->power_monitor, NM_POWER_MONITOR_SLEEPING, G_CALLBACK(sleeping_cb), self);
+    g_signal_connect(priv->power_monitor, NM_POWER_MONITOR_SHUTDOWN, G_CALLBACK(shutdown_cb), self);
 
     /* Listen for authorization changes */
     priv->auth_mgr = g_object_ref(nm_auth_manager_get());
@@ -8979,9 +9045,10 @@ get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
     case PROP_CHECKPOINTS:
         g_value_take_boxed(
             value,
-            priv->checkpoint_mgr ? nm_strv_make_deep_copied(
-                nm_checkpoint_manager_get_checkpoint_paths(priv->checkpoint_mgr, NULL))
-                                 : NULL);
+            priv->checkpoint_mgr
+                ? nm_strv_make_deep_copied(
+                      nm_checkpoint_manager_get_checkpoint_paths(priv->checkpoint_mgr, NULL))
+                : NULL);
         break;
     default:
         G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
@@ -9134,9 +9201,9 @@ dispose(GObject *object)
         nm_clear_pointer(&priv->sleep_devices, g_hash_table_unref);
     }
 
-    if (priv->sleep_monitor) {
-        g_signal_handlers_disconnect_by_func(priv->sleep_monitor, sleeping_cb, self);
-        g_clear_object(&priv->sleep_monitor);
+    if (priv->power_monitor) {
+        g_signal_handlers_disconnect_by_func(priv->power_monitor, sleeping_cb, self);
+        g_clear_object(&priv->power_monitor);
     }
 
     if (priv->fw_monitor) {
@@ -9765,3 +9832,11 @@ nm_manager_class_init(NMManagerClass *manager_class)
                                                    1,
                                                    NM_TYPE_DEVICE);
 }
+
+NMConfig *
+nm_manager_get_config(NMManager *self)
+{
+    NMManagerPrivate *priv = NM_MANAGER_GET_PRIVATE(self);
+
+    return priv->config;
+}
diff --git a/src/core/nm-manager.h b/src/core/nm-manager.h
index 3028eb7e..3c5213c4 100644
--- a/src/core/nm-manager.h
+++ b/src/core/nm-manager.h
@@ -10,6 +10,7 @@
 #include "settings/nm-settings-connection.h"
 #include "c-list/src/c-list.h"
 #include "nm-dbus-manager.h"
+#include "nm-config-data.h"
 
 #define NM_TYPE_MANAGER            (nm_manager_get_type())
 #define NM_MANAGER(obj)            (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MANAGER, NMManager))
@@ -266,4 +267,6 @@ gboolean nm_manager_devcon_autoconnect_blocked_reason_set(NMManager            *
                                                           NMSettingsAutoconnectBlockedReason value,
                                                           gboolean                           set);
 
+NMConfig *nm_manager_get_config(NMManager *self);
+
 #endif /* __NETWORKMANAGER_MANAGER_H__ */
diff --git a/src/core/nm-policy.c b/src/core/nm-policy.c
index feea97b1..60d11c75 100644
--- a/src/core/nm-policy.c
+++ b/src/core/nm-policy.c
@@ -1805,7 +1805,7 @@ unblock_autoconnect_for_ports(NMPolicy   *self,
 
         s_slave_con =
             nm_settings_connection_get_setting(sett_conn, NM_META_SETTING_TYPE_CONNECTION);
-        slave_master = nm_setting_connection_get_master(s_slave_con);
+        slave_master = nm_setting_connection_get_controller(s_slave_con);
         if (!slave_master)
             continue;
 
diff --git a/src/core/nm-sleep-monitor.c b/src/core/nm-power-monitor.c
index 66ea2f6c..289b0ff0 100644
--- a/src/core/nm-sleep-monitor.c
+++ b/src/core/nm-power-monitor.c
@@ -6,7 +6,7 @@
 
 #include "src/core/nm-default-daemon.h"
 
-#include "nm-sleep-monitor.h"
+#include "nm-power-monitor.h"
 
 #include <sys/stat.h>
 #include <gio/gunixfdlist.h>
@@ -15,24 +15,15 @@
 #include "libnm-core-intern/nm-core-internal.h"
 #include "NetworkManagerUtils.h"
 
-#if defined(SUSPEND_RESUME_UPOWER)
-
-#define SUSPEND_DBUS_NAME      "org.freedesktop.UPower"
-#define SUSPEND_DBUS_PATH      "/org/freedesktop/UPower"
-#define SUSPEND_DBUS_INTERFACE "org.freedesktop.UPower"
-#define USE_UPOWER             1
-#define _NMLOG_PREFIX_NAME     "sleep-monitor-up"
-
-#elif defined(SUSPEND_RESUME_SYSTEMD) || defined(SUSPEND_RESUME_ELOGIND)
+#if defined(SUSPEND_RESUME_SYSTEMD) || defined(SUSPEND_RESUME_ELOGIND)
 
 #define SUSPEND_DBUS_NAME      "org.freedesktop.login1"
 #define SUSPEND_DBUS_PATH      "/org/freedesktop/login1"
 #define SUSPEND_DBUS_INTERFACE "org.freedesktop.login1.Manager"
-#define USE_UPOWER             0
 #if defined(SUSPEND_RESUME_SYSTEMD)
-#define _NMLOG_PREFIX_NAME "sleep-monitor-sd"
+#define _NMLOG_PREFIX_NAME "power-monitor-sd"
 #else
-#define _NMLOG_PREFIX_NAME "sleep-monitor-el"
+#define _NMLOG_PREFIX_NAME "power-monitor-el"
 #endif
 
 #elif defined(SUSPEND_RESUME_CONSOLEKIT)
@@ -44,12 +35,11 @@
 #define SUSPEND_DBUS_NAME      "org.freedesktop.ConsoleKit"
 #define SUSPEND_DBUS_PATH      "/org/freedesktop/ConsoleKit/Manager"
 #define SUSPEND_DBUS_INTERFACE "org.freedesktop.ConsoleKit.Manager"
-#define USE_UPOWER             0
-#define _NMLOG_PREFIX_NAME     "sleep-monitor-ck"
+#define _NMLOG_PREFIX_NAME     "power-monitor-ck"
 
 #else
 
-#error define one of SUSPEND_RESUME_SYSTEMD, SUSPEND_RESUME_ELOGIND, SUSPEND_RESUME_CONSOLEKIT, or SUSPEND_RESUME_UPOWER
+#error define one of SUSPEND_RESUME_SYSTEMD, SUSPEND_RESUME_ELOGIND, SUSPEND_RESUME_CONSOLEKIT
 
 #endif
 
@@ -57,12 +47,13 @@
 
 enum {
     SLEEPING,
+    SHUTDOWN,
     LAST_SIGNAL,
 };
 
 static guint signals[LAST_SIGNAL] = {0};
 
-struct _NMSleepMonitor {
+struct _NMPowerMonitor {
     GObject parent;
 
     GDBusProxy *proxy;
@@ -76,13 +67,14 @@ struct _NMSleepMonitor {
 
     gulong sig_id_1;
     gulong sig_id_2;
+    gulong sig_id_3;
 };
 
-struct _NMSleepMonitorClass {
+struct _NMPowerMonitorClass {
     GObjectClass parent;
 };
 
-G_DEFINE_TYPE(NMSleepMonitor, nm_sleep_monitor, G_TYPE_OBJECT);
+G_DEFINE_TYPE(NMPowerMonitor, nm_power_monitor, G_TYPE_OBJECT);
 
 /*****************************************************************************/
 
@@ -91,28 +83,8 @@ G_DEFINE_TYPE(NMSleepMonitor, nm_sleep_monitor, G_TYPE_OBJECT);
 
 /*****************************************************************************/
 
-static void sleep_signal(NMSleepMonitor *self, gboolean is_about_to_suspend);
-
-/*****************************************************************************/
-
-#if USE_UPOWER
-
-static void
-upower_sleeping_cb(GDBusProxy *proxy, gpointer user_data)
-{
-    sleep_signal(user_data, TRUE);
-}
-
-static void
-upower_resuming_cb(GDBusProxy *proxy, gpointer user_data)
-{
-    sleep_signal(user_data, FALSE);
-}
-
-#else  /* USE_UPOWER */
-
 static void
-drop_inhibitor(NMSleepMonitor *self, gboolean force)
+drop_inhibitor(NMPowerMonitor *self, gboolean force)
 {
     if (!force && self->handles_active)
         return;
@@ -135,7 +107,7 @@ static void
 inhibit_done(GObject *source, GAsyncResult *result, gpointer user_data)
 {
     GDBusProxy                  *proxy   = G_DBUS_PROXY(source);
-    NMSleepMonitor              *self    = user_data;
+    NMPowerMonitor              *self    = user_data;
     gs_free_error GError        *error   = NULL;
     gs_unref_variant GVariant   *res     = NULL;
     gs_unref_object GUnixFDList *fd_list = NULL;
@@ -161,9 +133,9 @@ inhibit_done(GObject *source, GAsyncResult *result, gpointer user_data)
 }
 
 static void
-take_inhibitor(NMSleepMonitor *self)
+take_inhibitor(NMPowerMonitor *self)
 {
-    g_return_if_fail(NM_IS_SLEEP_MONITOR(self));
+    g_return_if_fail(NM_IS_POWER_MONITOR(self));
     g_return_if_fail(G_IS_DBUS_PROXY(self->proxy));
 
     drop_inhibitor(self, TRUE);
@@ -186,16 +158,10 @@ take_inhibitor(NMSleepMonitor *self)
 }
 
 static void
-prepare_for_sleep_cb(GDBusProxy *proxy, gboolean is_about_to_suspend, gpointer data)
-{
-    sleep_signal(data, is_about_to_suspend);
-}
-
-static void
 name_owner_cb(GObject *object, GParamSpec *pspec, gpointer user_data)
 {
     GDBusProxy     *proxy = G_DBUS_PROXY(object);
-    NMSleepMonitor *self  = NM_SLEEP_MONITOR(user_data);
+    NMPowerMonitor *self  = NM_POWER_MONITOR(user_data);
     char           *owner;
 
     g_assert(proxy == self->proxy);
@@ -207,59 +173,64 @@ name_owner_cb(GObject *object, GParamSpec *pspec, gpointer user_data)
         drop_inhibitor(self, TRUE);
     g_free(owner);
 }
-#endif /* USE_UPOWER */
 
 static void
-sleep_signal(NMSleepMonitor *self, gboolean is_about_to_suspend)
+prepare_for_sleep_cb(GDBusProxy *proxy, gboolean is_about_to_suspend, NMPowerMonitor *self)
 {
-    g_return_if_fail(NM_IS_SLEEP_MONITOR(self));
+    g_return_if_fail(NM_IS_POWER_MONITOR(self));
 
     _LOGD("received %s signal", is_about_to_suspend ? "SLEEP" : "RESUME");
 
-#if !USE_UPOWER
     if (!is_about_to_suspend)
         take_inhibitor(self);
-#endif
 
     g_signal_emit(self, signals[SLEEPING], 0, is_about_to_suspend);
 
-#if !USE_UPOWER
     if (is_about_to_suspend)
         drop_inhibitor(self, FALSE);
-#endif
+}
+
+static void
+prepare_for_shutdown_cb(GDBusProxy *proxy, gboolean is_about_to_shutdown, NMPowerMonitor *self)
+{
+    g_return_if_fail(NM_IS_POWER_MONITOR(self));
+
+    _LOGD("received SHUTDOWN signal");
+
+    g_signal_emit(self, signals[SHUTDOWN], 0);
 }
 
 /**
- * nm_sleep_monitor_inhibit_take:
- * @self: the #NMSleepMonitor instance
+ * nm_power_monitor_inhibit_take:
+ * @self: the #NMPowerMonitor instance
  *
  * Prevent the release of inhibitor lock
  *
  * Returns: an inhibitor handle that must be returned via
- *   nm_sleep_monitor_inhibit_release().
+ *   nm_power_monitor_inhibit_release().
  **/
-NMSleepMonitorInhibitorHandle *
-nm_sleep_monitor_inhibit_take(NMSleepMonitor *self)
+NMPowerMonitorInhibitorHandle *
+nm_power_monitor_inhibit_take(NMPowerMonitor *self)
 {
-    g_return_val_if_fail(NM_IS_SLEEP_MONITOR(self), NULL);
+    g_return_val_if_fail(NM_IS_POWER_MONITOR(self), NULL);
 
     self->handles_active = g_slist_prepend(self->handles_active, NULL);
-    return (NMSleepMonitorInhibitorHandle *) self->handles_active;
+    return (NMPowerMonitorInhibitorHandle *) self->handles_active;
 }
 
 /**
- * nm_sleep_monitor_inhibit_release:
- * @self: the #NMSleepMonitor instance
- * @handle: the #NMSleepMonitorInhibitorHandle inhibitor handle.
+ * nm_power_monitor_inhibit_release:
+ * @self: the #NMPowerMonitor instance
+ * @handle: the #NMPowerMonitorInhibitorHandle inhibitor handle.
  *
  * Allow again the release of inhibitor lock
  **/
 void
-nm_sleep_monitor_inhibit_release(NMSleepMonitor *self, NMSleepMonitorInhibitorHandle *handle)
+nm_power_monitor_inhibit_release(NMPowerMonitor *self, NMPowerMonitorInhibitorHandle *handle)
 {
     GSList *l;
 
-    g_return_if_fail(NM_IS_SLEEP_MONITOR(self));
+    g_return_if_fail(NM_IS_POWER_MONITOR(self));
     g_return_if_fail(handle);
 
     l = (GSList *) handle;
@@ -273,13 +244,11 @@ nm_sleep_monitor_inhibit_release(NMSleepMonitor *self, NMSleepMonitorInhibitorHa
 
     self->handles_active = g_slist_delete_link(self->handles_active, l);
 
-#if !USE_UPOWER
     drop_inhibitor(self, FALSE);
-#endif
 }
 
 static void
-on_proxy_acquired(GObject *object, GAsyncResult *res, NMSleepMonitor *self)
+on_proxy_acquired(GObject *object, GAsyncResult *res, NMPowerMonitor *self)
 {
     GError     *error = NULL;
     GDBusProxy *proxy;
@@ -294,18 +263,6 @@ on_proxy_acquired(GObject *object, GAsyncResult *res, NMSleepMonitor *self)
     self->proxy = proxy;
     g_clear_object(&self->cancellable);
 
-#if USE_UPOWER
-    self->sig_id_1 = _nm_dbus_proxy_signal_connect(self->proxy,
-                                                   "Sleeping",
-                                                   NULL,
-                                                   G_CALLBACK(upower_sleeping_cb),
-                                                   self);
-    self->sig_id_2 = _nm_dbus_proxy_signal_connect(self->proxy,
-                                                   "Resuming",
-                                                   NULL,
-                                                   G_CALLBACK(upower_resuming_cb),
-                                                   self);
-#else
     self->sig_id_1 =
         g_signal_connect(self->proxy, "notify::g-name-owner", G_CALLBACK(name_owner_cb), self);
     self->sig_id_2 = _nm_dbus_proxy_signal_connect(self->proxy,
@@ -313,6 +270,12 @@ on_proxy_acquired(GObject *object, GAsyncResult *res, NMSleepMonitor *self)
                                                    G_VARIANT_TYPE("(b)"),
                                                    G_CALLBACK(prepare_for_sleep_cb),
                                                    self);
+    self->sig_id_3 = _nm_dbus_proxy_signal_connect(self->proxy,
+                                                   "PrepareForShutdown",
+                                                   G_VARIANT_TYPE("(b)"),
+                                                   G_CALLBACK(prepare_for_shutdown_cb),
+                                                   self);
+
     {
         gs_free char *owner = NULL;
 
@@ -320,13 +283,12 @@ on_proxy_acquired(GObject *object, GAsyncResult *res, NMSleepMonitor *self)
         if (owner)
             take_inhibitor(self);
     }
-#endif
 }
 
 /*****************************************************************************/
 
 static void
-nm_sleep_monitor_init(NMSleepMonitor *self)
+nm_power_monitor_init(NMPowerMonitor *self)
 {
     self->inhibit_fd  = -1;
     self->cancellable = g_cancellable_new();
@@ -342,34 +304,33 @@ nm_sleep_monitor_init(NMSleepMonitor *self)
                              self);
 }
 
-NMSleepMonitor *
-nm_sleep_monitor_new(void)
+NMPowerMonitor *
+nm_power_monitor_new(void)
 {
-    return g_object_new(NM_TYPE_SLEEP_MONITOR, NULL);
+    return g_object_new(NM_TYPE_POWER_MONITOR, NULL);
 }
 
 static void
 dispose(GObject *object)
 {
-    NMSleepMonitor *self = NM_SLEEP_MONITOR(object);
+    NMPowerMonitor *self = NM_POWER_MONITOR(object);
 
-#if !USE_UPOWER
     drop_inhibitor(self, TRUE);
-#endif
 
     nm_clear_g_cancellable(&self->cancellable);
 
     if (self->proxy) {
         nm_clear_g_signal_handler(self->proxy, &self->sig_id_1);
         nm_clear_g_signal_handler(self->proxy, &self->sig_id_2);
+        nm_clear_g_signal_handler(self->proxy, &self->sig_id_3);
         g_clear_object(&self->proxy);
     }
 
-    G_OBJECT_CLASS(nm_sleep_monitor_parent_class)->dispose(object);
+    G_OBJECT_CLASS(nm_power_monitor_parent_class)->dispose(object);
 }
 
 static void
-nm_sleep_monitor_class_init(NMSleepMonitorClass *klass)
+nm_power_monitor_class_init(NMPowerMonitorClass *klass)
 {
     GObjectClass *gobject_class;
 
@@ -377,8 +338,8 @@ nm_sleep_monitor_class_init(NMSleepMonitorClass *klass)
 
     gobject_class->dispose = dispose;
 
-    signals[SLEEPING] = g_signal_new(NM_SLEEP_MONITOR_SLEEPING,
-                                     NM_TYPE_SLEEP_MONITOR,
+    signals[SLEEPING] = g_signal_new(NM_POWER_MONITOR_SLEEPING,
+                                     NM_TYPE_POWER_MONITOR,
                                      G_SIGNAL_RUN_LAST,
                                      0,
                                      NULL,
@@ -387,4 +348,13 @@ nm_sleep_monitor_class_init(NMSleepMonitorClass *klass)
                                      G_TYPE_NONE,
                                      1,
                                      G_TYPE_BOOLEAN);
+    signals[SHUTDOWN] = g_signal_new(NM_POWER_MONITOR_SHUTDOWN,
+                                     NM_TYPE_POWER_MONITOR,
+                                     G_SIGNAL_RUN_LAST,
+                                     0,
+                                     NULL,
+                                     NULL,
+                                     NULL,
+                                     G_TYPE_NONE,
+                                     0);
 }
diff --git a/src/core/nm-power-monitor.h b/src/core/nm-power-monitor.h
new file mode 100644
index 00000000..269bb98a
--- /dev/null
+++ b/src/core/nm-power-monitor.h
@@ -0,0 +1,34 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2012 - 2016 Red Hat, Inc.
+ * Author: Matthias Clasen <mclasen@redhat.com>
+ */
+
+#ifndef __NETWORKMANAGER_POWER_MONITOR_H__
+#define __NETWORKMANAGER_POWER_MONITOR_H__
+
+#define NM_TYPE_POWER_MONITOR (nm_power_monitor_get_type())
+#define NM_POWER_MONITOR(o) \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((o), NM_TYPE_POWER_MONITOR, NMPowerMonitor))
+#define NM_POWER_MONITOR_CLASS(k) \
+    (G_TYPE_CHECK_CLASS_CAST((k), NM_TYPE_POWER_MONITOR, NMPowerMonitorClass))
+#define NM_POWER_MONITOR_GET_CLASS(o) \
+    (G_TYPE_INSTANCE_GET_CLASS((o), NM_TYPE_POWER_MONITOR, NMPowerMonitorClass))
+#define NM_IS_POWER_MONITOR(o)       (G_TYPE_CHECK_INSTANCE_TYPE((o), NM_TYPE_POWER_MONITOR))
+#define NM_IS_POWER_MONITOR_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE((k), NM_TYPE_POWER_MONITOR))
+
+#define NM_POWER_MONITOR_SLEEPING "sleeping"
+#define NM_POWER_MONITOR_SHUTDOWN "shutdown"
+
+typedef struct _NMPowerMonitorClass NMPowerMonitorClass;
+
+GType nm_power_monitor_get_type(void) G_GNUC_CONST;
+
+NMPowerMonitor *nm_power_monitor_new(void);
+
+typedef struct _NMPowerMonitorInhibitorHandle NMPowerMonitorInhibitorHandle;
+
+NMPowerMonitorInhibitorHandle *nm_power_monitor_inhibit_take(NMPowerMonitor *self);
+void nm_power_monitor_inhibit_release(NMPowerMonitor *self, NMPowerMonitorInhibitorHandle *handle);
+
+#endif /* __NETWORKMANAGER_POWER_MONITOR_H__ */
diff --git a/src/core/nm-sleep-monitor.h b/src/core/nm-sleep-monitor.h
deleted file mode 100644
index 0b7708db..00000000
--- a/src/core/nm-sleep-monitor.h
+++ /dev/null
@@ -1,33 +0,0 @@
-/* SPDX-License-Identifier: GPL-2.0-or-later */
-/*
- * Copyright (C) 2012 - 2016 Red Hat, Inc.
- * Author: Matthias Clasen <mclasen@redhat.com>
- */
-
-#ifndef __NETWORKMANAGER_SLEEP_MONITOR_H__
-#define __NETWORKMANAGER_SLEEP_MONITOR_H__
-
-#define NM_TYPE_SLEEP_MONITOR (nm_sleep_monitor_get_type())
-#define NM_SLEEP_MONITOR(o) \
-    (_NM_G_TYPE_CHECK_INSTANCE_CAST((o), NM_TYPE_SLEEP_MONITOR, NMSleepMonitor))
-#define NM_SLEEP_MONITOR_CLASS(k) \
-    (G_TYPE_CHECK_CLASS_CAST((k), NM_TYPE_SLEEP_MONITOR, NMSleepMonitorClass))
-#define NM_SLEEP_MONITOR_GET_CLASS(o) \
-    (G_TYPE_INSTANCE_GET_CLASS((o), NM_TYPE_SLEEP_MONITOR, NMSleepMonitorClass))
-#define NM_IS_SLEEP_MONITOR(o)       (G_TYPE_CHECK_INSTANCE_TYPE((o), NM_TYPE_SLEEP_MONITOR))
-#define NM_IS_SLEEP_MONITOR_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE((k), NM_TYPE_SLEEP_MONITOR))
-
-#define NM_SLEEP_MONITOR_SLEEPING "sleeping"
-
-typedef struct _NMSleepMonitorClass NMSleepMonitorClass;
-
-GType nm_sleep_monitor_get_type(void) G_GNUC_CONST;
-
-NMSleepMonitor *nm_sleep_monitor_new(void);
-
-typedef struct _NMSleepMonitorInhibitorHandle NMSleepMonitorInhibitorHandle;
-
-NMSleepMonitorInhibitorHandle *nm_sleep_monitor_inhibit_take(NMSleepMonitor *self);
-void nm_sleep_monitor_inhibit_release(NMSleepMonitor *self, NMSleepMonitorInhibitorHandle *handle);
-
-#endif /* __NETWORKMANAGER_SLEEP_MONITOR_H__ */
diff --git a/src/core/nm-types.h b/src/core/nm-types.h
index 104a2f4b..6dfdc8e2 100644
--- a/src/core/nm-types.h
+++ b/src/core/nm-types.h
@@ -33,7 +33,7 @@ typedef struct _NMRfkillManager         NMRfkillManager;
 typedef struct _NMPacrunnerManager      NMPacrunnerManager;
 typedef struct _NMSessionMonitor        NMSessionMonitor;
 typedef struct _NMKeepAlive             NMKeepAlive;
-typedef struct _NMSleepMonitor          NMSleepMonitor;
+typedef struct _NMPowerMonitor          NMPowerMonitor;
 typedef struct _NMLldpListener          NMLldpListener;
 typedef struct _NMConfigDeviceStateData NMConfigDeviceStateData;
 
@@ -96,6 +96,6 @@ typedef struct _NMSecretAgent        NMSecretAgent;
 typedef struct _NMSettings           NMSettings;
 typedef struct _NMSettingsConnection NMSettingsConnection;
 
-#define NM_SETTING_CONNECTION_MDNS_UNKNOWN ((NMSettingConnectionMdns) -42)
+#define NM_SETTING_CONNECTION_MDNS_UNKNOWN ((NMSettingConnectionMdns) - 42)
 
 #endif /* NM_TYPES_H */
diff --git a/src/core/settings/nm-settings-connection.c b/src/core/settings/nm-settings-connection.c
index 176cc2c2..459c60ad 100644
--- a/src/core/settings/nm-settings-connection.c
+++ b/src/core/settings/nm-settings-connection.c
@@ -226,6 +226,29 @@ static guint _get_seen_bssids(NMSettingsConnection *self,
 
 /*****************************************************************************/
 
+char *
+nm_settings_connection_persist_mode_to_string(NMSettingsConnectionPersistMode mode)
+{
+    switch (mode) {
+    case NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY:
+        return "in-memory";
+    case NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY_DETACHED:
+        return "in-memory-detached";
+    case NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY_ONLY:
+        return "in-memory-only";
+    case NM_SETTINGS_CONNECTION_PERSIST_MODE_KEEP:
+        return "keep";
+    case NM_SETTINGS_CONNECTION_PERSIST_MODE_NO_PERSIST:
+        return "no-persist";
+    case NM_SETTINGS_CONNECTION_PERSIST_MODE_TO_DISK:
+        return "to-disk";
+    }
+
+    return nm_assert_unreachable_val(NULL);
+}
+
+/*****************************************************************************/
+
 NMSettings *
 nm_settings_connection_get_settings(NMSettingsConnection *self)
 {
diff --git a/src/core/settings/nm-settings-connection.h b/src/core/settings/nm-settings-connection.h
index 835a978e..d15a75b7 100644
--- a/src/core/settings/nm-settings-connection.h
+++ b/src/core/settings/nm-settings-connection.h
@@ -379,4 +379,8 @@ void _nm_settings_connection_emit_signal_updated_internal(
 
 void _nm_settings_connection_cleanup_after_remove(NMSettingsConnection *self);
 
+/*****************************************************************************/
+
+char *nm_settings_connection_persist_mode_to_string(NMSettingsConnectionPersistMode mode);
+
 #endif /* __NETWORKMANAGER_SETTINGS_CONNECTION_H__ */
diff --git a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-reader.c b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-reader.c
index 3bcbb71b..7c2b2026 100644
--- a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-reader.c
+++ b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-reader.c
@@ -297,7 +297,7 @@ check_if_bond_slave(shvarFile *ifcfg, NMSettingConnection *s_con)
         v = svGetValueStr(ifcfg, "MASTER", &value);
 
     if (v) {
-        master = nm_setting_connection_get_master(s_con);
+        master = nm_setting_connection_get_controller(s_con);
         if (master) {
             PARSE_WARNING("Already configured as slave of %s. Ignoring MASTER{_UUID}=\"%s\"",
                           master,
@@ -331,7 +331,7 @@ check_if_team_slave(shvarFile *ifcfg, NMSettingConnection *s_con)
     if (!v)
         return;
 
-    master = nm_setting_connection_get_master(s_con);
+    master = nm_setting_connection_get_controller(s_con);
     if (master) {
         PARSE_WARNING("Already configured as slave of %s. Ignoring TEAM_MASTER{_UUID}=\"%s\"",
                       master,
@@ -507,7 +507,7 @@ make_connection_setting(const char *file,
     if (v) {
         const char *old_value;
 
-        if ((old_value = nm_setting_connection_get_master(s_con))) {
+        if ((old_value = nm_setting_connection_get_controller(s_con))) {
             PARSE_WARNING("Already configured as slave of %s. Ignoring BRIDGE=\"%s\"",
                           old_value,
                           v);
@@ -530,7 +530,7 @@ make_connection_setting(const char *file,
     if (v) {
         const char *old_value;
 
-        if ((old_value = nm_setting_connection_get_master(s_con))) {
+        if ((old_value = nm_setting_connection_get_controller(s_con))) {
             PARSE_WARNING("Already configured as slave of %s. Ignoring OVS_PORT=\"%s\"",
                           old_value,
                           v);
@@ -550,7 +550,7 @@ make_connection_setting(const char *file,
     if (v) {
         const char *old_value;
 
-        if ((old_value = nm_setting_connection_get_master(s_con))) {
+        if ((old_value = nm_setting_connection_get_controller(s_con))) {
             PARSE_WARNING("Already configured as slave of %s. Ignoring VRF{_UUID}=\"%s\"",
                           old_value,
                           v);
@@ -4129,6 +4129,10 @@ next:
     v = svGetValueStr(ifcfg, "IEEE_8021X_PHASE2_CA_PATH", &value);
     g_object_set(s_8021x, NM_SETTING_802_1X_PHASE2_CA_PATH, v, NULL);
 
+    nm_clear_g_free(&value);
+    v = svGetValueStr(ifcfg, "IEEE_8021X_OPENSSL_CIPHERS", &value);
+    g_object_set(s_8021x, NM_SETTING_802_1X_OPENSSL_CIPHERS, v, NULL);
+
     g_object_set(s_8021x,
                  NM_SETTING_802_1X_OPTIONAL,
                  svGetValueBoolean(ifcfg, "IEEE_8021X_OPTIONAL", FALSE),
diff --git a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.c b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.c
index b4edefbb..277d0d5f 100644
--- a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.c
+++ b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.c
@@ -828,6 +828,7 @@ const NMSIfcfgKeyTypeInfo nms_ifcfg_well_known_keys[] = {
     _KEY_TYPE("IEEE_8021X_INNER_PRIVATE_KEY", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("IEEE_8021X_INNER_PRIVATE_KEY_PASSWORD", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("IEEE_8021X_INNER_PRIVATE_KEY_PASSWORD_FLAGS", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
+    _KEY_TYPE("IEEE_8021X_OPENSSL_CIPHERS", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("IEEE_8021X_OPTIONAL", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("IEEE_8021X_PAC_FILE", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("IEEE_8021X_PASSWORD", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
diff --git a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.h b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.h
index eb9e418a..4e56a258 100644
--- a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.h
+++ b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.h
@@ -33,7 +33,7 @@ typedef struct {
     NMSIfcfgKeyTypeFlags key_flags;
 } NMSIfcfgKeyTypeInfo;
 
-extern const NMSIfcfgKeyTypeInfo nms_ifcfg_well_known_keys[264];
+extern const NMSIfcfgKeyTypeInfo nms_ifcfg_well_known_keys[265];
 
 const NMSIfcfgKeyTypeInfo *nms_ifcfg_well_known_key_find_info(const char *key, gssize *out_idx);
 
diff --git a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-writer.c b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-writer.c
index 617c5ef6..f4598e2d 100644
--- a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-writer.c
+++ b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-writer.c
@@ -579,6 +579,10 @@ write_8021x_setting(NMConnection *connection,
                "IEEE_8021X_PIN_FLAGS",
                nm_setting_802_1x_get_pin_flags(s_8021x));
 
+    svSetValueStr(ifcfg,
+                  "IEEE_8021X_OPENSSL_CIPHERS",
+                  nm_setting_802_1x_get_openssl_ciphers(s_8021x));
+
     if (!write_8021x_certs(s_8021x, secrets, blobs, FALSE, ifcfg, error))
         return FALSE;
 
@@ -1138,7 +1142,7 @@ write_wired_setting_impl(NMSettingWired *s_wired, shvarFile *ifcfg, gboolean is_
                   "GENERATE_MAC_ADDRESS_MASK",
                   nm_setting_wired_get_generate_mac_address_mask(s_wired));
 
-    macaddr_blacklist = nm_setting_wired_get_mac_address_blacklist(s_wired);
+    macaddr_blacklist = nm_setting_wired_get_mac_address_denylist(s_wired);
     if (macaddr_blacklist[0]) {
         gs_free char *blacklist_str = NULL;
 
@@ -2247,7 +2251,7 @@ write_connection_setting(NMSettingConnection *s_con, shvarFile *ifcfg, const cha
     mud_url = nm_setting_connection_get_mud_url(s_con);
     svSetValue(ifcfg, "MUD_URL", mud_url);
 
-    master = nm_setting_connection_get_master(s_con);
+    master = nm_setting_connection_get_controller(s_con);
     if (master) {
         /* The reader prefers the *_UUID variants, however we still try to resolve
          * it into an interface name, so that legacy tooling is not confused. */
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-ibft-dhcp b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-ibft-dhcp
new file mode 100644
index 00000000..abfcd6e4
--- /dev/null
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-ibft-dhcp
@@ -0,0 +1,4 @@
+# Intel Corporation 82540EP Gigabit Ethernet Controller (Mobile)
+DEVICE=eth0
+HWADDR=00:33:21:98:b9:f1
+BOOTPROTO=ibft
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-ibft-static b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-ibft-static
new file mode 100644
index 00000000..99b02e42
--- /dev/null
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-ibft-static
@@ -0,0 +1,4 @@
+# Intel Corporation 82540EP Gigabit Ethernet Controller (Mobile)
+DEVICE=eth0
+HWADDR=00:33:21:98:b9:f0
+BOOTPROTO=ibft
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-wired-8021x-peap-mschapv2 b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-wired-8021x-peap-mschapv2
index 27bcbbf9..f32f234a 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-wired-8021x-peap-mschapv2
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-wired-8021x-peap-mschapv2
@@ -13,3 +13,4 @@ IEEE_8021X_PEAP_VERSION=1
 IEEE_8021X_PEAP_FORCE_NEW_LABEL=yes
 IEEE_8021X_INNER_AUTH_METHODS=MSCHAPV2
 IEEE_8021X_ANON_IDENTITY=somebody
+IEEE_8021X_OPENSSL_CIPHERS=DEFAULT@SECLEVEL=0
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/test-ifcfg-rh.c b/src/core/settings/plugins/ifcfg-rh/tests/test-ifcfg-rh.c
index 1e6b6c25..40a18853 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/test-ifcfg-rh.c
+++ b/src/core/settings/plugins/ifcfg-rh/tests/test-ifcfg-rh.c
@@ -2230,7 +2230,7 @@ test_clear_master(void)
 
     s_con = nmtst_connection_assert_setting(connection, NM_TYPE_SETTING_CONNECTION);
 
-    g_assert_cmpstr(nm_setting_connection_get_master(s_con), ==, "br0");
+    g_assert_cmpstr(nm_setting_connection_get_controller(s_con), ==, "br0");
     g_assert_cmpstr(nm_setting_connection_get_port_type(s_con), ==, "bridge");
 
     /* 2. write the connection to a new file */
@@ -2247,7 +2247,7 @@ test_clear_master(void)
                  NULL,
                  NULL);
 
-    g_assert_cmpstr(nm_setting_connection_get_master(s_con), ==, NULL);
+    g_assert_cmpstr(nm_setting_connection_get_controller(s_con), ==, NULL);
     g_assert_cmpstr(nm_setting_connection_get_port_type(s_con), ==, NULL);
 
     nmtst_assert_connection_verifies_after_normalization(connection, 0, 0);
@@ -5317,6 +5317,8 @@ test_write_wired_dhcp_8021x_peap_mschapv2(void)
                  "1",
                  NM_SETTING_802_1X_PHASE2_AUTH,
                  "mschapv2",
+                 NM_SETTING_802_1X_OPENSSL_CIPHERS,
+                 "DEFAULT@SECLEVEL=0",
                  NULL);
     nm_setting_802_1x_add_eap_method(s_8021x, "peap");
 
@@ -7668,7 +7670,7 @@ test_read_bridge_component(void)
                                        NULL);
 
     s_con = nmtst_connection_assert_setting(connection, NM_TYPE_SETTING_CONNECTION);
-    g_assert_cmpstr(nm_setting_connection_get_master(s_con), ==, "br0");
+    g_assert_cmpstr(nm_setting_connection_get_controller(s_con), ==, "br0");
     g_assert_cmpstr(nm_setting_connection_get_port_type(s_con), ==, NM_SETTING_BRIDGE_SETTING_NAME);
 
     s_port = nmtst_connection_assert_setting(connection, NM_TYPE_SETTING_BRIDGE_PORT);
@@ -8260,7 +8262,7 @@ test_read_bond_slave(void)
 
     s_con = nmtst_connection_assert_setting(connection, NM_TYPE_SETTING_CONNECTION);
 
-    g_assert_cmpstr(nm_setting_connection_get_master(s_con), ==, "bond0");
+    g_assert_cmpstr(nm_setting_connection_get_controller(s_con), ==, "bond0");
 
     g_assert_cmpstr(nm_setting_connection_get_port_type(s_con), ==, NM_SETTING_BOND_SETTING_NAME);
 }
@@ -8317,7 +8319,7 @@ test_read_bond_port(void)
         _connection_from_file(TEST_IFCFG_DIR "/ifcfg-test-bond-port", NULL, TYPE_ETHERNET, NULL);
 
     s_con = nmtst_connection_assert_setting(connection, NM_TYPE_SETTING_CONNECTION);
-    g_assert_cmpstr(nm_setting_connection_get_master(s_con), ==, "bond99");
+    g_assert_cmpstr(nm_setting_connection_get_controller(s_con), ==, "bond99");
     g_assert_cmpstr(nm_setting_connection_get_port_type(s_con), ==, NM_SETTING_BOND_SETTING_NAME);
 
     s_port = nmtst_connection_assert_setting(connection, NM_TYPE_SETTING_BOND_PORT);
@@ -8556,7 +8558,7 @@ test_read_bond_slave_ib(void)
 
     s_con = nmtst_connection_assert_setting(connection, NM_TYPE_SETTING_CONNECTION);
 
-    g_assert_cmpstr(nm_setting_connection_get_master(s_con), ==, "bond0");
+    g_assert_cmpstr(nm_setting_connection_get_controller(s_con), ==, "bond0");
     g_assert_cmpstr(nm_setting_connection_get_port_type(s_con), ==, NM_SETTING_BOND_SETTING_NAME);
 }
 
@@ -9096,7 +9098,7 @@ test_read_team_port(gconstpointer user_data)
     g_assert_cmpstr(nm_setting_connection_get_connection_type(s_con),
                     ==,
                     NM_SETTING_WIRED_SETTING_NAME);
-    g_assert_cmpstr(nm_setting_connection_get_master(s_con), ==, "team0");
+    g_assert_cmpstr(nm_setting_connection_get_controller(s_con), ==, "team0");
 
     s_team_port = nmtst_connection_assert_setting(connection, NM_TYPE_SETTING_TEAM_PORT);
     g_assert_cmpstr(nm_setting_team_port_get_config(s_team_port), ==, expected_config);
@@ -9224,7 +9226,7 @@ test_read_team_port_empty_config(void)
     g_assert_cmpstr(nm_setting_connection_get_connection_type(s_con),
                     ==,
                     NM_SETTING_WIRED_SETTING_NAME);
-    g_assert_cmpstr(nm_setting_connection_get_master(s_con), ==, "team0");
+    g_assert_cmpstr(nm_setting_connection_get_controller(s_con), ==, "team0");
 
     /* Normalization adds a team-port setting */
     s_team_port = nmtst_connection_assert_setting(connection, NM_TYPE_SETTING_TEAM_PORT);
diff --git a/src/core/settings/plugins/keyfile/tests/test-keyfile-settings.c b/src/core/settings/plugins/keyfile/tests/test-keyfile-settings.c
index b26bad69..e3e5f84f 100644
--- a/src/core/settings/plugins/keyfile/tests/test-keyfile-settings.c
+++ b/src/core/settings/plugins/keyfile/tests/test-keyfile-settings.c
@@ -91,7 +91,7 @@ assert_reread(NMConnection *connection, gboolean normalize_connection, const cha
     reread = keyfile_read_connection_from_file(testfile);
 
     if (!normalize_connection && (s_con = nm_connection_get_setting_connection(connection))
-        && !nm_setting_connection_get_master(s_con)
+        && !nm_setting_connection_get_controller(s_con)
         && !nm_connection_get_setting_proxy(connection)) {
         connection_clone = nmtst_clone_connection(connection);
         connection       = connection_clone;
@@ -2003,7 +2003,7 @@ test_read_bridge_component(void)
     g_assert(s_con);
     g_assert_cmpstr(nm_setting_connection_get_id(s_con), ==, expected_id);
     g_assert_cmpstr(nm_setting_connection_get_uuid(s_con), ==, expected_uuid);
-    g_assert_cmpstr(nm_setting_connection_get_master(s_con), ==, "br0");
+    g_assert_cmpstr(nm_setting_connection_get_controller(s_con), ==, "br0");
     g_assert_cmpstr(nm_setting_connection_get_port_type(s_con), ==, NM_SETTING_BRIDGE_SETTING_NAME);
 
     s_wired = nm_connection_get_setting_wired(connection);
diff --git a/src/core/supplicant/nm-supplicant-config.c b/src/core/supplicant/nm-supplicant-config.c
index 9ad4a8f9..7db614a2 100644
--- a/src/core/supplicant/nm-supplicant-config.c
+++ b/src/core/supplicant/nm-supplicant-config.c
@@ -1811,6 +1811,9 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self,
     value = nm_setting_802_1x_get_anonymous_identity(setting);
     if (!add_string_val(self, value, "anonymous_identity", FALSE, NULL, error))
         return FALSE;
+    value = nm_setting_802_1x_get_openssl_ciphers(setting);
+    if (value && !add_string_val(self, value, "openssl_ciphers", FALSE, NULL, error))
+        return FALSE;
 
     return TRUE;
 }
diff --git a/src/core/supplicant/nm-supplicant-settings-verify.c b/src/core/supplicant/nm-supplicant-settings-verify.c
index 7842365c..c7aaf47d 100644
--- a/src/core/supplicant/nm-supplicant-settings-verify.c
+++ b/src/core/supplicant/nm-supplicant-settings-verify.c
@@ -93,6 +93,7 @@ static const struct Opt opt_table[] = {
     OPT_BYTES("mka_cak", 65536),
     OPT_BYTES("mka_ckn", 65536),
     OPT_BYTES("nai", 0),
+    OPT_BYTES("openssl_ciphers", 0),
     OPT_INT("owe_only", 0, 1),
     OPT_BYTES("pac_file", 0),
     OPT_KEYWORD("pairwise", NM_MAKE_STRV("CCMP", "TKIP", "GCMP-256", "NONE", )),
diff --git a/src/core/tests/config/test-config.c b/src/core/tests/config/test-config.c
index 054b9003..2b27d535 100644
--- a/src/core/tests/config/test-config.c
+++ b/src/core/tests/config/test-config.c
@@ -1076,7 +1076,7 @@ _set_values_intern_atomic_section_2_set(NMConfig            *config,
     g_key_file_set_value(keyfile,
                          NM_CONFIG_KEYFILE_GROUPPREFIX_INTERN "with-whitespace",
                          "key2",
-                         " b c\\,  d  ");
+                         " b c\\\\,  d  ");
     *out_expected_changes = NM_CONFIG_CHANGE_CAUSE_SET_VALUES | NM_CONFIG_CHANGE_VALUES
                             | NM_CONFIG_CHANGE_VALUES_INTERN;
 }
diff --git a/src/core/tests/test-dcb.c b/src/core/tests/test-dcb.c
index f85e90f9..33437974 100644
--- a/src/core/tests/test-dcb.c
+++ b/src/core/tests/test-dcb.c
@@ -11,7 +11,7 @@
 
 typedef struct {
     guint       num;
-    const char *cmds[];
+    const char *cmds[16];
 } DcbExpected;
 
 static gboolean
diff --git a/src/core/vpn/nm-vpn-connection.c b/src/core/vpn/nm-vpn-connection.c
index 3dba9ff6..de0c9f71 100644
--- a/src/core/vpn/nm-vpn-connection.c
+++ b/src/core/vpn/nm-vpn-connection.c
@@ -2099,20 +2099,12 @@ _dbus_signal_ip_config_cb(NMVpnConnection *self, int addr_family, GVariant *dict
                                                  NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4))
                 nm_l3_config_data_add_route(l3cd, addr_family, route, NULL);
         }
-    } else if (IS_IPv4 ? g_variant_lookup(dict, NM_VPN_PLUGIN_IP4_CONFIG_ROUTES, "aau", &var_iter)
-                       : g_variant_lookup(dict,
-                                          NM_VPN_PLUGIN_IP6_CONFIG_ROUTES,
-                                          "a(ayuayu)",
-                                          &var_iter)) {
-        _nm_unused nm_auto_free_variant_iter GVariantIter *var_iter_ref_owner = var_iter;
-        NMPlatformIPXRoute                                 route              = {};
-        guint32                                            plen;
-        GVariant                                          *next_hop;
-        GVariant                                          *dest;
-        guint32                                            prefix;
-        guint32                                            metric;
+    } else if (IS_IPv4) {
+        if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP4_CONFIG_ROUTES, "aau", &var_iter)) {
+            _nm_unused nm_auto_free_variant_iter GVariantIter *var_iter_ref_owner = var_iter;
+            NMPlatformIPXRoute                                 route              = {};
+            guint32                                            plen;
 
-        if (IS_IPv4) {
             while (g_variant_iter_next(var_iter, "@au", &v)) {
                 _nm_unused gs_unref_variant GVariant *v_ref_owner = v;
 
@@ -2151,42 +2143,84 @@ _dbus_signal_ip_config_cb(NMVpnConnection *self, int addr_family, GVariant *dict
                     break;
                 }
             }
-        } else {
-            while (
-                g_variant_iter_next(var_iter, "(@ayu@ayu)", &dest, &prefix, &next_hop, &metric)) {
-                _nm_unused gs_unref_variant GVariant *next_hop_ref_owner = next_hop;
-                _nm_unused gs_unref_variant GVariant *dest_ref_owner     = dest;
+        }
+    } else {
+        _nm_unused nm_auto_free_variant_iter GVariantIter *var_iter_ref_owner = NULL;
+        NMPlatformIPXRoute                                 route              = {};
+        guint32                                            prefix;
+        guint32                                            metric;
+        NMOptionBool                                       new_signature = NM_OPTION_BOOL_DEFAULT;
+
+        /* IPv6 and no "preserve-routes" */
+
+        if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP6_CONFIG_ROUTES, "a(ayuayu)", &var_iter))
+            new_signature = FALSE;
+        else if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP6_CONFIG_ROUTES, "a(ayuayuay)", &var_iter))
+            new_signature = TRUE;
+        else
+            var_iter = NULL;
+
+        var_iter_ref_owner = var_iter;
+
+        while (TRUE) {
+            gs_unref_variant GVariant *next_hop = NULL;
+            gs_unref_variant GVariant *dest     = NULL;
+            gs_unref_variant GVariant *pref_src = NULL;
+
+            if (new_signature == NM_OPTION_BOOL_DEFAULT) {
+                break;
+            } else if (new_signature) {
+                if (!g_variant_iter_next(var_iter,
+                                         "(@ayu@ayu@ay)",
+                                         &dest,
+                                         &prefix,
+                                         &next_hop,
+                                         &metric,
+                                         &pref_src))
+                    break;
+            } else {
+                if (!g_variant_iter_next(var_iter,
+                                         "(@ayu@ayu)",
+                                         &dest,
+                                         &prefix,
+                                         &next_hop,
+                                         &metric))
+                    break;
+            }
 
-                if (prefix > 128)
-                    continue;
+            if (prefix > 128)
+                continue;
 
-                route.r6 = (NMPlatformIP6Route){
-                    .plen       = prefix,
-                    .table_any  = TRUE,
-                    .metric_any = TRUE,
-                    .rt_source  = NM_IP_CONFIG_SOURCE_VPN,
-                };
+            route.r6 = (NMPlatformIP6Route){
+                .plen       = prefix,
+                .table_any  = TRUE,
+                .metric_any = TRUE,
+                .rt_source  = NM_IP_CONFIG_SOURCE_VPN,
+            };
 
-                if (!nm_ip_addr_set_from_variant(AF_INET6, &route.r6.network, dest, NULL))
-                    continue;
+            if (!nm_ip_addr_set_from_variant(AF_INET6, &route.r6.network, dest, NULL))
+                continue;
 
-                nm_ip_addr_set_from_variant(AF_INET6, &route.r6.gateway, next_hop, NULL);
+            if (pref_src
+                && !nm_ip_addr_set_from_variant(AF_INET6, &route.r6.pref_src, pref_src, NULL))
+                continue;
 
-                nm_ip6_addr_clear_host_address(&route.r6.network, &route.r6.network, route.r6.plen);
+            nm_ip_addr_set_from_variant(AF_INET6, &route.r6.gateway, next_hop, NULL);
 
-                if (!IN6_IS_ADDR_UNSPECIFIED(&priv->ip_data_6.gw_external.addr6)
-                    && IN6_ARE_ADDR_EQUAL(&route.r6.network, &priv->ip_data_6.gw_external.addr6)
-                    && route.r6.plen == 128) {
-                    /* Ignore host routes to the VPN gateway since NM adds one itself.
-                     * Since NM knows more about the routing situation than the VPN
-                     * server, we want to use the NM created route instead of whatever
-                     * the server provides.
-                     */
-                    continue;
-                }
+            nm_ip6_addr_clear_host_address(&route.r6.network, &route.r6.network, route.r6.plen);
 
-                nm_l3_config_data_add_route_6(l3cd, &route.r6);
+            if (!IN6_IS_ADDR_UNSPECIFIED(&priv->ip_data_6.gw_external.addr6)
+                && IN6_ARE_ADDR_EQUAL(&route.r6.network, &priv->ip_data_6.gw_external.addr6)
+                && route.r6.plen == 128) {
+                /* Ignore host routes to the VPN gateway since NM adds one itself.
+                 * Since NM knows more about the routing situation than the VPN
+                 * server, we want to use the NM created route instead of whatever
+                 * the server provides.
+                 */
+                continue;
             }
+
+            nm_l3_config_data_add_route_6(l3cd, &route.r6);
         }
     }