about summary refs log tree commit diff
path: root/src/core/nm-core-utils.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/core/nm-core-utils.c')
-rw-r--r--src/core/nm-core-utils.c470
1 files changed, 470 insertions, 0 deletions
diff --git a/src/core/nm-core-utils.c b/src/core/nm-core-utils.c
index deac04e7..ee05bedd 100644
--- a/src/core/nm-core-utils.c
+++ b/src/core/nm-core-utils.c
@@ -21,6 +21,10 @@
 #include <linux/if_infiniband.h>
 #include <net/if_arp.h>
 #include <net/ethernet.h>
+#include <netinet/ip_icmp.h>
+#include <netinet/icmp6.h>
+#include <netinet/ip6.h>
+#include <linux/if_packet.h>
 
 #include "libnm-glib-aux/nm-uuid.h"
 #include "libnm-platform/nmp-base.h"
@@ -5003,6 +5007,469 @@ NM_UTILS_LOOKUP_STR_DEFINE(nm_activation_type_to_string,
 /*****************************************************************************/
 
 typedef struct {
+    NMIPAddrTyped address;
+    char         *addr_str;
+    GTask        *task;
+    GSource      *timeout_source;
+    GSource      *retry_source;
+    GSource      *input_source;
+    gulong        cancellable_id;
+    int           ifindex;
+    int           socket;
+    guint16       seq;
+} PingInfo;
+
+#define _NMLOG2_PREFIX_NAME "ping"
+#define _NMLOG2_DOMAIN      LOGD_CORE
+#define _NMLOG2(level, info, ...)                                                          \
+    G_STMT_START                                                                           \
+    {                                                                                      \
+        if (nm_logging_enabled((level), (_NMLOG2_DOMAIN))) {                               \
+            PingInfo *_info = (info);                                                      \
+                                                                                           \
+            _nm_log((level),                                                               \
+                    (_NMLOG2_DOMAIN),                                                      \
+                    0,                                                                     \
+                    NULL,                                                                  \
+                    NULL,                                                                  \
+                    _NMLOG2_PREFIX_NAME "[" NM_HASH_OBFUSCATE_PTR_FMT                      \
+                                        ",if=%d,%s]: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \
+                    NM_HASH_OBFUSCATE_PTR(_info),                                          \
+                    _info->ifindex,                                                        \
+                    _info->addr_str _NM_UTILS_MACRO_REST(__VA_ARGS__));                    \
+        }                                                                                  \
+    }                                                                                      \
+    G_STMT_END
+
+static void
+ping_complete(PingInfo *info, GError *error)
+{
+    nm_clear_g_cancellable_disconnect(g_task_get_cancellable(info->task), &info->cancellable_id);
+
+    if (error && !nm_utils_error_is_cancelled(error)) {
+        _LOG2T(info, "terminated with error: %s", error->message);
+    }
+
+    if (error)
+        g_task_return_error(info->task, error);
+    else
+        g_task_return_boolean(info->task, TRUE);
+
+    nm_clear_g_source_inst(&info->timeout_source);
+    nm_clear_g_source_inst(&info->retry_source);
+    nm_clear_g_source_inst(&info->input_source);
+    nm_clear_g_free(&info->addr_str);
+    nm_clear_fd(&info->socket);
+    g_object_unref(info->task);
+
+    g_free(info);
+}
+
+static gboolean
+ping_socket_data_cb(int fd, GIOCondition condition, gpointer user_data)
+{
+    PingInfo *info = user_data;
+    ssize_t   len;
+    union {
+        struct icmphdr   icmph;
+        struct icmp6_hdr icmp6h;
+    } pkt;
+
+    len = recv(fd, &pkt, sizeof(pkt), 0);
+
+    if (len < 0)
+        return G_SOURCE_CONTINUE;
+
+    if (info->address.addr_family == AF_INET) {
+        if (len >= sizeof(struct icmphdr) && pkt.icmph.type == ICMP_ECHOREPLY) {
+            _LOG2T(info, "received echo-reply with seq %hu", ntohs(pkt.icmph.un.echo.sequence));
+            ping_complete(info, NULL);
+            return G_SOURCE_CONTINUE;
+        }
+    } else {
+        if (len >= sizeof(struct icmp6_hdr) && pkt.icmp6h.icmp6_type == ICMP6_ECHO_REPLY) {
+            _LOG2T(info, "received echo-reply with seq %hu", ntohs(pkt.icmp6h.icmp6_seq));
+            ping_complete(info, NULL);
+            return G_SOURCE_CONTINUE;
+        }
+    }
+
+    return G_SOURCE_CONTINUE;
+}
+
+static void
+ping_send(PingInfo *info)
+{
+    const bool IS_IPv4 = NM_IS_IPv4(info->address.addr_family);
+    union {
+        struct sockaddr_in6 sa6;
+        struct sockaddr_in  sa4;
+    } sa;
+    union {
+        struct icmphdr   icmph;
+        struct icmp6_hdr icmp6h;
+    } pkt;
+    socklen_t sa_len;
+    size_t    pkt_len;
+    nm_be32_t ifindex_be;
+    int       errsv;
+
+    info->seq++;
+
+    if (info->socket < 0) {
+        info->socket = socket(info->address.addr_family,
+                              SOCK_DGRAM | SOCK_CLOEXEC,
+                              IS_IPv4 ? IPPROTO_ICMP : IPPROTO_ICMPV6);
+        if (info->socket < 0) {
+            errsv = errno;
+            _LOG2T(info, "socket creation failed: %s", nm_strerror_native(errsv));
+            /* Try again at the next iteration */
+            return;
+        }
+
+        memset(&sa, 0, sizeof(sa));
+        if (IS_IPv4) {
+            sa.sa4.sin_family      = AF_INET;
+            sa.sa4.sin_addr.s_addr = info->address.addr.addr4;
+            sa_len                 = sizeof(struct sockaddr_in);
+        } else {
+            sa.sa6.sin6_family = AF_INET6;
+            sa.sa6.sin6_addr   = info->address.addr.addr6;
+            if (IN6_IS_ADDR_LINKLOCAL(&info->address.addr.addr6))
+                sa.sa6.sin6_scope_id = info->ifindex;
+            sa_len = sizeof(struct sockaddr_in6);
+        }
+
+        /* setsockopt(IP*_UNICAST_IF) must be called *before* connecting
+         * the socket, otherwise it doesn't have any effect */
+        ifindex_be = htonl(info->ifindex);
+        if (setsockopt(info->socket,
+                       IS_IPv4 ? IPPROTO_IP : IPPROTO_IPV6,
+                       IS_IPv4 ? IP_UNICAST_IF : IPV6_UNICAST_IF,
+                       &ifindex_be,
+                       sizeof(ifindex_be))) {
+            errsv = errno;
+            _LOG2T(info,
+                   "failed to bind the socket to the interface: %s",
+                   nm_strerror_native(errsv));
+            /* Try again at the next iteration */
+            nm_clear_fd(&info->socket);
+            return;
+        }
+
+        /* Connect the socket so that the kernel only delivers us packets
+         * coming from the given remote address */
+        if (connect(info->socket, (struct sockaddr *) &sa, sa_len) < 0) {
+            errsv = errno;
+            _LOG2T(info, "failed to connect the socket: %s", nm_strerror_native(errsv));
+            /* try again at the next iteration */
+            nm_clear_fd(&info->socket);
+            return;
+        }
+
+        info->input_source = nm_g_unix_fd_source_new(info->socket,
+                                                     G_IO_IN,
+                                                     G_PRIORITY_DEFAULT,
+                                                     ping_socket_data_cb,
+                                                     info,
+                                                     NULL);
+        g_source_attach(info->input_source, g_task_get_context(info->task));
+    }
+
+    if (IS_IPv4) {
+        memset(&pkt.icmph, 0, sizeof(struct icmphdr));
+        pkt.icmph.type             = ICMP_ECHO;
+        pkt.icmph.un.echo.sequence = htons(info->seq);
+        pkt_len                    = sizeof(struct icmphdr);
+    } else {
+        memset(&pkt.icmp6h, 0, sizeof(struct icmp6_hdr));
+        pkt.icmp6h.icmp6_type = ICMP6_ECHO_REQUEST;
+        pkt.icmp6h.icmp6_seq  = htons(info->seq);
+        pkt_len               = sizeof(struct icmp6_hdr);
+    }
+    /* The kernel will automatically set the ID ICMP field and filter
+     * incoming packets by the same ID */
+
+    if (send(info->socket, &pkt, pkt_len, 0) < 0) {
+        errsv = errno;
+        _LOG2T(info, "error sending echo-request #%u: %s", info->seq, nm_strerror_native(errsv));
+        return;
+    }
+
+    _LOG2T(info, "sent echo-request #%u", info->seq);
+}
+
+static gboolean
+ping_timeout_cb(gpointer user_data)
+{
+    PingInfo *info = user_data;
+
+    _LOG2T(info, "timeout");
+
+    nm_clear_g_source_inst(&info->timeout_source);
+    ping_complete(info, g_error_new_literal(NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, "timeout"));
+
+    return G_SOURCE_CONTINUE;
+}
+
+static gboolean
+ping_retry_cb(gpointer user_data)
+{
+    PingInfo *info = user_data;
+
+    ping_send(info);
+
+    return G_SOURCE_CONTINUE;
+}
+
+static void
+ping_cancelled(GObject *object, gpointer user_data)
+{
+    PingInfo *info  = user_data;
+    GError   *error = NULL;
+
+    nm_clear_g_signal_handler(g_task_get_cancellable(info->task), &info->cancellable_id);
+    nm_utils_error_set_cancelled(&error, FALSE, NULL);
+    ping_complete(info, error);
+}
+
+void
+nm_utils_ping_host(NMIPAddrTyped       address,
+                   int                 ifindex,
+                   guint               timeout_sec,
+                   GCancellable       *cancellable,
+                   GAsyncReadyCallback callback,
+                   gpointer            cb_data)
+{
+    PingInfo *info;
+    char      buf[NM_INET_ADDRSTRLEN];
+    gulong    signal_id;
+
+    nm_assert(ifindex > 0);
+    nm_assert(G_IS_CANCELLABLE(cancellable));
+    nm_assert(callback);
+    nm_assert(cb_data);
+
+    info          = g_new0(PingInfo, 1);
+    info->address = address;
+    info->ifindex = ifindex;
+    info->task    = nm_g_task_new(NULL, cancellable, nm_utils_ping_host, callback, cb_data);
+    info->socket  = -1;
+
+    nm_inet_ntop(address.addr_family, address.addr.addr_ptr, buf);
+    info->addr_str = g_strdup(buf);
+
+    _LOG2T(info, "started");
+
+    if (timeout_sec > 0) {
+        info->timeout_source = nm_g_timeout_source_new_seconds(timeout_sec,
+                                                               G_PRIORITY_DEFAULT,
+                                                               ping_timeout_cb,
+                                                               info,
+                                                               NULL);
+        g_source_attach(info->timeout_source, g_task_get_context(info->task));
+    }
+
+    info->retry_source =
+        nm_g_timeout_source_new_seconds(1, G_PRIORITY_DEFAULT, ping_retry_cb, info, NULL);
+    g_source_attach(info->retry_source, g_task_get_context(info->task));
+
+    signal_id = g_cancellable_connect(cancellable, G_CALLBACK(ping_cancelled), info, NULL);
+    if (signal_id == 0) {
+        /* the callback was invoked synchronously, which destroyed @info.
+         * We must not touch it anymore. */
+        return;
+    }
+    info->cancellable_id = signal_id;
+
+    ping_send(info);
+}
+
+gboolean
+nm_utils_ping_host_finish(GAsyncResult *result, GError **error)
+{
+    GTask *task = G_TASK(result);
+
+    nm_assert(nm_g_task_is_valid(result, NULL, nm_utils_ping_host));
+
+    return g_task_propagate_boolean(task, error);
+}
+
+/*****************************************************************************/
+
+/*
+ * nm_utils_icmp6_checksum:
+ * @ip6_src: pointer to the IPv6 source address
+ * @data_len: length of the data
+ * @data: the data on which to compute the checksum
+ *
+ * Computes the ICMP6 checksum over @data (with length @data_len) and the IPv6
+ * pseudo-header. @ip6_src points to the source address in the IPv6 header.
+ */
+uint16_t
+nm_utils_icmp6_checksum(const void *ip6_src, size_t data_len, const void *data)
+{
+    uint32_t        sum = 0;
+    const uint16_t *ptr;
+    const uint8_t  *ptr8;
+    size_t          i;
+
+    /* Pseudo-header: source address */
+    ptr = (const uint16_t *) ip6_src;
+    for (i = 0; i < 8; i++)
+        sum += *ptr++;
+
+    /* Pseudo-header: destination address */
+    for (i = 0; i < 8; i++)
+        sum += *ptr++;
+
+    /* Pseudo-header: payload length */
+    sum += htons(data_len);
+
+    /* Pseudo-header: next header */
+    sum += htons(IPPROTO_ICMPV6);
+
+    /* ICMPv6 data */
+    ptr = (const uint16_t *) data;
+    for (i = 0; i < data_len / 2; i++)
+        sum += ptr[i];
+
+    /* Handle odd byte */
+    if (data_len % 2) {
+        ptr8 = &((const uint8_t *) data)[data_len - 1];
+        sum += htons((guint16) (*ptr8) << 8);
+    }
+
+    /* Fold 32-bit sum to 16 bits */
+    while (sum >> 16)
+        sum = (sum & 0xffff) + (sum >> 16);
+
+    return (uint16_t) ~sum;
+}
+
+/*
+ * nm_utils_ipv6_dad_send:
+ * @addr: the target IPv6 address
+ * @ifindex: the interface index
+ * @arptype: the ARP hardware type of the interface (e.g. ARPHRD_ETHER, ARPHRD_NONE)
+ *
+ * Send an IPv6 Duplicate Address Detection (DAD) Neighbor Solicitation
+ * for the given address.
+ *
+ * Returns: %TRUE if the packet was sent successfully, %FALSE on error
+ */
+gboolean
+nm_utils_ipv6_dad_send(const struct in6_addr *addr, int ifindex, int arptype)
+{
+    /* DAD packet: IPv6 header + ICMPv6 NS + nonce option (RFC 3971) */
+    struct _nm_packed {
+        struct ip6_hdr             ip6h;
+        struct nd_neighbor_solicit ns;
+        guint8                     ns_opt_nr;
+        guint8                     ns_opt_len;
+        guint8                     ns_opt_nonce[6];
+    } dad_pkt;
+    nm_auto_close int fd = -1;
+    int               errsv;
+    char              sbuf[NM_INET_ADDRSTRLEN];
+
+    nm_assert(addr);
+    nm_assert(ifindex > 0);
+
+    /* IPv6 header */
+    dad_pkt.ip6h = (struct ip6_hdr) {
+        .ip6_flow        = htonl(6 << 28), /* version 6, tclass 0, flowlabel 0 */
+        .ip6_plen        = htons(sizeof(dad_pkt) - sizeof(struct ip6_hdr)),
+        .ip6_nxt         = IPPROTO_ICMPV6,
+        .ip6_hlim        = 255,
+        .ip6_src         = IN6ADDR_ANY_INIT,
+        .ip6_dst.s6_addr = {0xff,
+                            0x02,
+                            0,
+                            0,
+                            0,
+                            0,
+                            0,
+                            0,
+                            0,
+                            0,
+                            0,
+                            0x01,
+                            0xff,
+                            addr->s6_addr[13],
+                            addr->s6_addr[14],
+                            addr->s6_addr[15]},
+    };
+
+    /* ICMPv6 Neighbor Solicitation */
+    dad_pkt.ns = (struct nd_neighbor_solicit) {
+        .nd_ns_type   = ND_NEIGHBOR_SOLICIT,
+        .nd_ns_target = *addr,
+    };
+
+    /* Nonce option (RFC 3971) */
+    dad_pkt.ns_opt_nr  = 14;
+    dad_pkt.ns_opt_len = 1; /* in units of 8 bytes */
+    nm_random_get_bytes(dad_pkt.ns_opt_nonce, sizeof(dad_pkt.ns_opt_nonce));
+
+    /* Compute the ICMPv6 checksum */
+    dad_pkt.ns.nd_ns_cksum = nm_utils_icmp6_checksum(&dad_pkt.ip6h.ip6_src,
+                                                     sizeof(dad_pkt) - sizeof(struct ip6_hdr),
+                                                     &dad_pkt.ns);
+
+    /* We need a ETH_P_IPV6 socket because we need to use a zero IPv6 source address */
+    fd = socket(AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC, htons(ETH_P_IPV6));
+    if (fd < 0) {
+        errsv = errno;
+        nm_log_warn(LOGD_CORE,
+                    "ipv6-dad: failed to create socket for %s: %s",
+                    nm_inet6_ntop(addr, sbuf),
+                    nm_strerror_native(errsv));
+        return FALSE;
+    }
+
+    /* Build link-layer destination address. For Ethernet, use the solicited-node
+     * multicast MAC address. For L3-only devices (ARPHRD_NONE, ARPHRD_RAWIP, etc.)
+     * there is no L2 header, so set sll_halen to 0. */
+    {
+        struct sockaddr_ll dst_ll = {
+            .sll_family   = AF_PACKET,
+            .sll_protocol = htons(ETH_P_IPV6),
+            .sll_ifindex  = ifindex,
+        };
+
+        if (arptype == ARPHRD_ETHER) {
+            dst_ll.sll_halen   = ETH_ALEN;
+            dst_ll.sll_addr[0] = 0x33;
+            dst_ll.sll_addr[1] = 0x33;
+            dst_ll.sll_addr[2] = 0xff;
+            dst_ll.sll_addr[3] = addr->s6_addr[13];
+            dst_ll.sll_addr[4] = addr->s6_addr[14];
+            dst_ll.sll_addr[5] = addr->s6_addr[15];
+        }
+
+        if (sendto(fd, &dad_pkt, sizeof(dad_pkt), 0, (struct sockaddr *) &dst_ll, sizeof(dst_ll))
+            < 0) {
+            errsv = errno;
+            nm_log_warn(LOGD_CORE,
+                        "ipv6-dad: failed to send DAD NS for %s: %s",
+                        nm_inet6_ntop(addr, sbuf),
+                        nm_strerror_native(errsv));
+            return FALSE;
+        }
+    }
+
+    nm_log_dbg(LOGD_CORE,
+               "ipv6-dad: sent DAD NS for %s on ifindex %d",
+               nm_inet6_ntop(addr, sbuf),
+               ifindex);
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+typedef struct {
     GPid     pid;
     GTask   *task;
     gulong   cancellable_id;
@@ -5023,6 +5490,9 @@ typedef struct {
     gsize    out_buffer_offset;
 } HelperInfo;
 
+#undef _NMLOG2_PREFIX_NAME
+#undef _NMLOG2_DOMAIN
+#undef _NMLOG2
 #define _NMLOG2_PREFIX_NAME "nm-daemon-helper"
 #define _NMLOG2_DOMAIN      LOGD_CORE
 #define _NMLOG2(level, info, ...)                                                    \