core: take over IPv6LL address management if kernel supports it (bgo #734149)

NM keeps interfaces IFF_UP when possible to receive link layer
events like carrier changes.  Unfortunately, the kernel also
uses IFF_UP as a flag to assign an IPv6LL address to the interface,
which results in IPv6 connectivity on the link even if the interface
is not supposed to be activated/connected.

NM sets disable_ipv6=1 to ensure that the kernel does not set up
IPv6LL connectivity on interfaces when they are not supposed to
be active and connected.  Unfortunately, that prevents users from
manually adding IPv6 addresses to the interface, since they expect
previous kernel behavior where IPv6 is enabled whenever the
interface is IFF_UP.

Furthermore, interfaces like PPP and some WWAN devices provide
misleading information to the kernel which causes the kernel to
create the wrong IPv6LL address for the interface.  The IPv6LL
address for these devices is obtained through control channels
instead (IPV6CP for PPP, proprietary protocols for WWAN devices)
and should be used instead of the kernel address.  So we'd like
to suppress kernel IPv6LL address generation on these interfaces
anyway.

This patch makes use of the netlink IFLA_INET6_ADDR_GEN_MODE
attribute to take over assignment of IPv6LL addresses while
keeping the interface IFF_UP, to ensure there is only IPv6
connectivity when the user requests it.

To remain compliant with standards, if a user adds IPv6 addresses
externally, NetworkManager must also immediately add an IPv6LL
address for that interface too.

https://bugzilla.gnome.org/show_bug.cgi?id=734149
This commit is contained in:
Dan Williams 2014-07-24 17:14:30 -05:00
parent 37f11fbdf8
commit 5e4761a3a9

View file

@ -268,6 +268,7 @@ typedef struct {
NMIP6Config * vpn6_config; /* routes added by a VPN which uses this device */
NMIP6Config * wwan_ip6_config;
NMIP6Config * ext_ip6_config; /* Stuff added outside NM */
gboolean nm_ipv6ll; /* TRUE if NM handles the device's IPv6LL address */
NMRDisc * rdisc;
gulong rdisc_changed_id;
@ -563,6 +564,9 @@ nm_device_set_ip_iface (NMDevice *self, const char *iface)
if (priv->ip_iface) {
priv->ip_ifindex = nm_platform_link_get_ifindex (priv->ip_iface);
if (priv->ip_ifindex > 0) {
if (nm_platform_check_support_user_ipv6ll ())
nm_platform_link_set_user_ipv6ll_enabled (priv->ip_ifindex, TRUE);
if (!nm_platform_link_is_up (priv->ip_ifindex))
nm_platform_link_set_up (priv->ip_ifindex);
} else {
@ -611,6 +615,12 @@ get_ip_iface_identifier (NMDevice *self, NMUtilsIPv6IfaceId *out_iid)
return success;
}
static gboolean
nm_device_get_ip_iface_identifier (NMDevice *self, NMUtilsIPv6IfaceId *iid)
{
return NM_DEVICE_GET_CLASS (self)->get_ip_iface_identifier (self, iid);
}
const char *
nm_device_get_driver (NMDevice *self)
{
@ -1132,7 +1142,12 @@ device_ip_link_changed (NMDevice *self, NMPlatformLink *info)
}
static void
link_changed_cb (NMPlatform *platform, int ifindex, NMPlatformLink *info, NMPlatformSignalChangeType change_type, NMPlatformReason reason, NMDevice *self)
link_changed_cb (NMPlatform *platform,
int ifindex,
NMPlatformLink *info,
NMPlatformSignalChangeType change_type,
NMPlatformReason reason,
NMDevice *self)
{
if (change_type != NM_PLATFORM_SIGNAL_CHANGED)
return;
@ -3368,6 +3383,52 @@ linklocal6_complete (NMDevice *self)
g_return_if_fail (FALSE);
}
static void
check_and_add_ipv6ll_addr (NMDevice *self)
{
NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE (self);
int ip_ifindex = nm_device_get_ip_ifindex (self);
NMUtilsIPv6IfaceId iid;
struct in6_addr lladdr;
guint i, n;
if (priv->nm_ipv6ll == FALSE)
return;
if (priv->ip6_config) {
n = nm_ip6_config_get_num_addresses (priv->ip6_config);
for (i = 0; i < n; i++) {
const NMPlatformIP6Address *addr;
addr = nm_ip6_config_get_address (priv->ip6_config, i);
if (IN6_IS_ADDR_LINKLOCAL (&addr->address)) {
/* Already have an LL address, nothing to do */
return;
}
}
}
if (!nm_device_get_ip_iface_identifier (self, &iid)) {
_LOGW (LOGD_IP6, "failed to get interface identifier; IPv6 may be broken");
return;
}
memset (&lladdr, 0, sizeof (lladdr));
lladdr.s6_addr16[0] = htons (0xfe80);
nm_utils_ipv6_addr_set_interface_identfier (&lladdr, iid);
_LOGD (LOGD_IP6, "adding IPv6LL address %s", nm_utils_inet6_ntop (&lladdr, NULL));
if (!nm_platform_ip6_address_add (ip_ifindex,
lladdr,
in6addr_any,
64,
NM_PLATFORM_LIFETIME_PERMANENT,
NM_PLATFORM_LIFETIME_PERMANENT,
0)) {
_LOGW (LOGD_IP6, "failed to add IPv6 link-local address %s",
nm_utils_inet6_ntop (&lladdr, NULL));
}
}
static NMActStageReturn
linklocal6_start (NMDevice *self)
{
@ -3386,6 +3447,8 @@ linklocal6_start (NMDevice *self)
method = nm_utils_get_ip_config_method (connection, NM_TYPE_SETTING_IP6_CONFIG);
_LOGD (LOGD_DEVICE, "linklocal6: starting IPv6 with method '%s', but the device has no link-local addresses configured. Wait.", method);
check_and_add_ipv6ll_addr (self);
priv->linklocal6_timeout_id = g_timeout_add_seconds (5, linklocal6_timeout_cb, self);
return NM_ACT_STAGE_RETURN_POSTPONE;
@ -3630,8 +3693,8 @@ addrconf6_start_with_link_ready (NMDevice *self)
g_assert (priv->rdisc);
if (!NM_DEVICE_GET_CLASS (self)->get_ip_iface_identifier (self, &iid)) {
_LOGW (LOGD_IP6, "failed to get interface identifier");
if (!nm_device_get_ip_iface_identifier (self, &iid)) {
_LOGW (LOGD_IP6, "failed to get interface identifier; IPv6 cannot continue");
return FALSE;
}
nm_rdisc_set_iid (priv->rdisc, iid);
@ -3759,8 +3822,39 @@ restore_ip6_properties (NMDevice *self)
gpointer key, value;
g_hash_table_iter_init (&iter, priv->ip6_saved_properties);
while (g_hash_table_iter_next (&iter, &key, &value))
while (g_hash_table_iter_next (&iter, &key, &value)) {
/* Don't touch "disable_ipv6" if we're doing userland IPv6LL */
if (priv->nm_ipv6ll && strcmp (key, "disable_ipv6") == 0)
continue;
nm_device_ipv6_sysctl_set (self, key, value);
}
}
static inline void
set_disable_ipv6 (NMDevice *self, const char *value)
{
/* We only touch disable_ipv6 when NM is not managing the IPv6LL address */
if (NM_DEVICE_GET_PRIVATE (self)->nm_ipv6ll == FALSE)
nm_device_ipv6_sysctl_set (self, "disable_ipv6", value);
}
static inline void
set_nm_ipv6ll (NMDevice *self, gboolean enable)
{
NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE (self);
int ifindex = nm_device_get_ip_ifindex (self);
if (!nm_platform_check_support_user_ipv6ll ())
return;
priv->nm_ipv6ll = enable;
if (ifindex > 0) {
const char *detail = enable ? "enable" : "disable";
_LOGD (LOGD_IP6, "will %s userland IPv6LL", detail);
if (!nm_platform_link_set_user_ipv6ll_enabled (ifindex, enable))
_LOGW (LOGD_IP6, "failed to %s userspace IPv6LL address handling", detail);
}
}
static NMSettingIP6ConfigPrivacy
@ -3889,7 +3983,7 @@ act_stage3_ip6_config_start (NMDevice *self,
}
/* Re-enable IPv6 on the interface */
nm_device_ipv6_sysctl_set (self, "disable_ipv6", "0");
set_disable_ipv6 (self, "0");
/* Enable/disable IPv6 Privacy Extensions.
* If a global value is configured by sysadmin (e.g. /etc/sysctl.conf),
@ -5908,17 +6002,29 @@ queued_ip_config_change (gpointer user_data)
priv->queued_ip_config_id = 0;
update_ip_config (self, FALSE);
/* If no IPv6 link-local address exists but other addresses do then we
* must add the LL address to remain conformant with RFC 3513 chapter 2.1
* ("Addressing Model"): "All interfaces are required to have at least
* one link-local unicast address".
*/
if (priv->ip6_config && nm_ip6_config_get_num_addresses (priv->ip6_config))
check_and_add_ipv6ll_addr (self);
return FALSE;
}
static void
device_ip_changed (NMPlatform *platform, int ifindex, gpointer platform_object, NMPlatformSignalChangeType change_type, NMPlatformReason reason, gpointer user_data)
device_ip_changed (NMPlatform *platform,
int ifindex,
gpointer platform_object,
NMPlatformSignalChangeType change_type,
NMPlatformReason reason,
NMDevice *self)
{
NMDevice *self = user_data;
NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE (self);
if (nm_device_get_ip_ifindex (self) == ifindex) {
NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE (self);
if (!priv->queued_ip_config_id)
priv->queued_ip_config_id = g_idle_add (queued_ip_config_change, self);
@ -6538,7 +6644,7 @@ nm_device_cleanup (NMDevice *self, NMDeviceStateReason reason)
_cleanup_generic_pre (self, TRUE);
/* Turn off kernel IPv6 */
nm_device_ipv6_sysctl_set (self, "disable_ipv6", "1");
set_disable_ipv6 (self, "1");
nm_device_ipv6_sysctl_set (self, "accept_ra", "0");
nm_device_ipv6_sysctl_set (self, "use_tempaddr", "0");
@ -6653,6 +6759,7 @@ _set_state_full (NMDevice *self,
if (nm_device_get_act_request (self))
nm_device_cleanup (self, reason);
nm_device_take_down (self, TRUE);
set_nm_ipv6ll (self, FALSE);
restore_ip6_properties (self);
}
break;
@ -6660,7 +6767,8 @@ _set_state_full (NMDevice *self,
if (old_state == NM_DEVICE_STATE_UNMANAGED) {
save_ip6_properties (self);
if (reason != NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED) {
nm_device_ipv6_sysctl_set (self, "disable_ipv6", "1");
set_nm_ipv6ll (self, TRUE);
set_disable_ipv6 (self, "1");
nm_device_ipv6_sysctl_set (self, "accept_ra_defrtr", "0");
nm_device_ipv6_sysctl_set (self, "accept_ra_pinfo", "0");
nm_device_ipv6_sysctl_set (self, "accept_ra_rtr_pref", "0");
@ -6685,6 +6793,12 @@ _set_state_full (NMDevice *self,
nm_device_cleanup (self, reason);
break;
case NM_DEVICE_STATE_DISCONNECTED:
/* Ensure devices that previously assumed a connection now have
* userspace IPv6LL enabled.
*/
if (reason != NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED)
set_nm_ipv6ll (self, TRUE);
if (old_state > NM_DEVICE_STATE_UNAVAILABLE)
nm_device_cleanup (self, reason);
break;
@ -7179,6 +7293,13 @@ constructor (GType type,
g_signal_connect (platform, NM_PLATFORM_SIGNAL_IP6_ROUTE_CHANGED, G_CALLBACK (device_ip_changed), self);
g_signal_connect (platform, NM_PLATFORM_SIGNAL_LINK_CHANGED, G_CALLBACK (link_changed_cb), self);
if (nm_platform_check_support_user_ipv6ll ()) {
int ip_ifindex = nm_device_get_ip_ifindex (self);
if (ip_ifindex > 0)
priv->nm_ipv6ll = nm_platform_link_get_user_ipv6ll_enabled (ip_ifindex);
}
return object;
error:
@ -7257,6 +7378,9 @@ dispose (GObject *object)
g_warn_if_fail (priv->slaves == NULL);
g_assert (priv->master_ready_id == 0);
/* Let the kernel manage IPv6LL again */
set_nm_ipv6ll (self, FALSE);
_cleanup_generic_post (self, FALSE);
g_clear_pointer (&priv->ip6_saved_properties, g_hash_table_unref);