[calls] Introduce CallsNetworkWatch to notify of network changes



commit 3714f99d38c33c98dee0f3d13eb96af4bd122533
Author: Evangelos Ribeiro Tzaras <devrtz fortysixandtwo eu>
Date:   Sat Jul 31 12:38:58 2021 +0200

    Introduce CallsNetworkWatch to notify of network changes
    
    The libsofia-sip stack needs to bind to a specific interface when there are
    multiple network interfaces available.
    Handles should be recreated when the default route changes.

 src/calls-network-watch.c | 451 ++++++++++++++++++++++++++++++++++++++++++++++
 src/calls-network-watch.h |  39 ++++
 src/meson.build           |   1 +
 3 files changed, 491 insertions(+)
---
diff --git a/src/calls-network-watch.c b/src/calls-network-watch.c
new file mode 100644
index 00000000..49cee6ae
--- /dev/null
+++ b/src/calls-network-watch.c
@@ -0,0 +1,451 @@
+/*
+ * Copyright (C) 2021 Purism SPC
+ *
+ * This file is part of Calls.
+ *
+ * Calls is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Calls is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Calls. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Evangelos Ribeiro Tzaras <evangelos tzaras puri sm>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ */
+
+#define G_LOG_DOMAIN "CallsNetworkWatch"
+
+#include "calls-network-watch.h"
+
+#include <gio/gio.h>
+
+#include <arpa/inet.h>
+#include <linux/rtnetlink.h>
+
+/**
+ * SECTION:calls-network-watch
+ * @short_description: Watches network interfaces
+ * @Title: CallsNetworkWatch
+ *
+ * The #CallsNetworkWatch uses rtnetlink messages to keep
+ * track of network interfaces.
+ *
+ * This allows the sofia SIP stack to bind to the
+ * correct interface and can later help in deciding which codec to
+ * use for media (f.e. lower bandwidth on a metered connection).
+ */
+
+
+typedef struct {
+  struct nlmsghdr n;
+  struct rtmsg r;
+  char buf[1024];
+} RequestData;
+
+enum {
+  PROP_0,
+  PROP_IPV4,
+  PROP_IPV6,
+  PROP_LAST_PROP
+};
+static GParamSpec *props[PROP_LAST_PROP];
+
+enum {
+  NETWORK_CHANGED,
+  N_SIGNALS
+};
+static guint signals[N_SIGNALS];
+
+
+typedef struct _CallsNetworkWatch {
+  GObject parent;
+
+  RequestData *req;
+  int fd;
+  unsigned int seq;
+  char buf[1024]; /* buffer for responses to rtnetlink requests */
+
+  guint timeout_id;
+
+  char *ipv4;
+  char *ipv6;
+  char tmp_addr[INET6_ADDRSTRLEN];
+} CallsNetworkWatch;
+
+
+static void initable_iface_init (GInitableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (CallsNetworkWatch, calls_network_watch, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init))
+
+#define DUMMY_TARGET_V4 "1.2.3.4"
+#define DUMMY_TARGET_V6 "::1.2.3.4"
+
+static gboolean
+req_route_v4 (CallsNetworkWatch *self)
+{
+  int addr_len = 4;
+  int len = RTA_LENGTH (addr_len);
+  struct rtattr *rta;
+
+  g_assert (CALLS_IS_NETWORK_WATCH (self));
+
+  self->req->n.nlmsg_len = NLMSG_LENGTH (sizeof (struct rtmsg));
+  self->req->n.nlmsg_flags = NLM_F_REQUEST;
+  self->req->n.nlmsg_type = RTM_GETROUTE;
+  self->req->r.rtm_family = AF_INET;
+
+
+  rta = ((struct rtattr *) (((void *) (&self->req->n)) +
+                            NLMSG_ALIGN (self->req->n.nlmsg_len)));
+  rta->rta_type = RTA_DST;
+  rta->rta_len = len;
+
+  /* use a dummy target destination */
+  if (inet_pton (AF_INET, DUMMY_TARGET_V4, RTA_DATA (rta)) != 1)
+    return FALSE;
+
+  self->req->n.nlmsg_len = NLMSG_ALIGN (self->req->n.nlmsg_len) + RTA_ALIGN (len);
+
+  return TRUE;
+}
+
+
+static gboolean
+req_route_v6 (CallsNetworkWatch *self)
+{
+  int addr_len = 16;
+  int len = RTA_LENGTH (addr_len);
+  struct rtattr *rta;
+
+  g_assert (CALLS_IS_NETWORK_WATCH (self));
+
+  self->req->n.nlmsg_len = NLMSG_LENGTH (sizeof (struct rtmsg));
+  self->req->n.nlmsg_flags = NLM_F_REQUEST;
+  self->req->n.nlmsg_type = RTM_GETROUTE;
+  self->req->r.rtm_family = AF_INET6;
+
+  rta = ((struct rtattr *) (((void *) (&self->req->n)) +
+                            NLMSG_ALIGN (self->req->n.nlmsg_len)));
+  rta->rta_type = RTA_DST;
+  rta->rta_len = len;
+
+  /* use a dummy target destination */
+  if (inet_pton (AF_INET6, DUMMY_TARGET_V6, RTA_DATA (rta)) != 1)
+    return FALSE;
+
+  self->req->n.nlmsg_len = NLMSG_ALIGN (self->req->n.nlmsg_len) + RTA_ALIGN (len);
+
+  return TRUE;
+}
+
+#undef DUMMY_TARGET_V4
+#undef DUMMY_TARGET_V6
+
+static gboolean
+talk_rtnl (CallsNetworkWatch *self)
+{
+  struct iovec iov;
+  struct iovec riov;
+  struct sockaddr_nl nladdr = { .nl_family = AF_NETLINK };
+  struct msghdr msg = {
+    .msg_name = &nladdr,
+    .msg_namelen = sizeof (nladdr),
+    .msg_iov = &iov,
+    .msg_iovlen = 1,
+  };
+
+  struct nlmsghdr *h;
+  int status;
+
+  g_assert (CALLS_IS_NETWORK_WATCH (self));
+
+  iov.iov_base = (void *) &self->req->n;
+  iov.iov_len = self->req->n.nlmsg_len;
+  self->req->n.nlmsg_seq = self->seq++;
+
+  status = sendmsg (self->fd, &msg, 0);
+  if (status < 0) {
+    g_warning ("Could not send rtnetlink: %d", errno);
+    return FALSE;
+  }
+
+  /* change msg to use response iov */
+  riov.iov_base = self->buf;
+  riov.iov_len = sizeof (self->buf);
+
+  msg.msg_iov = &riov;
+  msg.msg_iovlen = 1;
+
+  status = recvmsg (self->fd, &msg, 0);
+
+  if (status == -1) {
+    g_warning ("Could not receive rtnetlink: %d", errno);
+    return FALSE;
+  }
+
+  h = (struct nlmsghdr *) self->buf;
+  if (h->nlmsg_type == NLMSG_ERROR) {
+    g_warning ("An error occured in the netlink stack");
+    return FALSE;
+  }
+
+  return TRUE;
+}
+
+static gboolean
+get_prefsrc (CallsNetworkWatch *self,
+             int                family)
+{
+  struct nlmsghdr *h;
+  struct rtmsg *r;
+  struct rtattr *rta;
+  int len;
+  gboolean found = FALSE;
+
+  g_assert (CALLS_IS_NETWORK_WATCH (self));
+
+  h = (struct nlmsghdr *) self->buf;
+  r = NLMSG_DATA (self->buf);
+  rta = RTM_RTA (r);
+  len = h->nlmsg_len - NLMSG_LENGTH (sizeof (*r));
+
+  while (RTA_OK (rta, len)) {
+    if (rta->rta_type == RTA_PREFSRC) {
+      found = TRUE;
+      break;
+    }
+    rta = RTA_NEXT (rta, len);
+  }
+
+  if (!found)
+    return FALSE;
+
+  if (family == AF_INET) {
+    inet_ntop (AF_INET, RTA_DATA (rta), self->tmp_addr, INET_ADDRSTRLEN);
+  } else if (family == AF_INET6) {
+    inet_ntop (AF_INET6, RTA_DATA (rta), self->tmp_addr, INET6_ADDRSTRLEN);
+  } else {
+    return FALSE;
+  }
+
+  return TRUE;
+}
+
+static gboolean
+fetch_ipv4 (CallsNetworkWatch *self)
+{
+  g_assert (CALLS_IS_NETWORK_WATCH (self));
+
+  if (!req_route_v4 (self))
+    return FALSE;
+
+  if (!talk_rtnl (self))
+    return FALSE;
+
+  return get_prefsrc (self, AF_INET);
+}
+
+static gboolean
+fetch_ipv6 (CallsNetworkWatch *self)
+{
+  g_assert (CALLS_IS_NETWORK_WATCH (self));
+
+  if (!req_route_v6 (self))
+    return FALSE;
+
+  if (!talk_rtnl (self))
+    return FALSE;
+
+  return get_prefsrc (self, AF_INET6);
+}
+
+
+static gboolean
+on_watch_network (CallsNetworkWatch *self)
+{
+  gboolean changed_v4 = FALSE;
+  gboolean changed_v6 = FALSE;
+
+  changed_v4 = fetch_ipv4 (self) && g_strcmp0 (self->tmp_addr, self->ipv4) != 0;
+  if (changed_v4) {
+    g_free (self->ipv4);
+    self->ipv4 = g_strdup (self->tmp_addr);
+    g_debug ("New IPv4: %s", self->ipv4);
+    g_object_notify_by_pspec (G_OBJECT (self), props[PROP_IPV4]);
+  }
+
+  changed_v6 = fetch_ipv6 (self) && g_strcmp0 (self->tmp_addr, self->ipv6) != 0;
+  if (changed_v6) {
+    g_free (self->ipv6);
+    self->ipv6 = g_strdup (self->tmp_addr);
+    g_debug ("New IPv6: %s", self->ipv6);
+    g_object_notify_by_pspec (G_OBJECT (self), props[PROP_IPV6]);
+  }
+
+  if (changed_v4 || changed_v6)
+    g_signal_emit (self, signals[NETWORK_CHANGED], 0);
+
+  return G_SOURCE_CONTINUE;
+}
+
+static void
+calls_network_watch_get_property (GObject *object,
+                                  guint    property_id,
+                                  GValue  *value,
+                                  GParamSpec *pspec)
+{
+  CallsNetworkWatch *self = CALLS_NETWORK_WATCH (object);
+
+  switch (property_id) {
+  case PROP_IPV4:
+    g_value_set_string (value, calls_network_watch_get_ipv4 (self));
+    break;
+
+  case PROP_IPV6:
+    g_value_set_string (value, calls_network_watch_get_ipv6 (self));
+    break;
+
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    break;
+  }
+}
+
+
+static void
+calls_network_watch_finalize (GObject *object)
+{
+  CallsNetworkWatch *self = CALLS_NETWORK_WATCH (object);
+
+  g_source_remove (self->timeout_id);
+  g_free (self->req);
+  g_free (self->ipv4);
+  g_free (self->ipv6);
+  close (self->fd);
+
+  G_OBJECT_CLASS (calls_network_watch_parent_class)->finalize (object);
+}
+
+static void
+calls_network_watch_class_init (CallsNetworkWatchClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->get_property = calls_network_watch_get_property;
+  object_class->finalize = calls_network_watch_finalize;
+
+  signals[NETWORK_CHANGED] = g_signal_new ("network-changed",
+                                           CALLS_TYPE_NETWORK_WATCH,
+                                           G_SIGNAL_RUN_LAST,
+                                           0,
+                                           NULL, NULL, NULL,
+                                           G_TYPE_NONE,
+                                           0);
+
+  props[PROP_IPV4] =
+    g_param_spec_string ("ipv4",
+                         "IPv4",
+                         "The preferred source address for IPv4",
+                         NULL,
+                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+  props[PROP_IPV6] =
+    g_param_spec_string ("ipv6",
+                         "IPv6",
+                         "The preferred source address for IPv6",
+                         NULL,
+                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+  g_object_class_install_properties (object_class, PROP_LAST_PROP, props);
+}
+
+
+static void
+calls_network_watch_init (CallsNetworkWatch *self)
+{
+  self->seq = time (NULL);
+  self->req = g_new0 (RequestData, 1);
+  self->timeout_id = g_timeout_add_seconds (15,
+                                            G_SOURCE_FUNC (on_watch_network),
+                                            self);
+}
+
+
+static gboolean
+calls_network_watch_initable_init (GInitable    *initable,
+                                   GCancellable *cancelable,
+                                   GError      **error)
+{
+  CallsNetworkWatch *self = CALLS_NETWORK_WATCH (initable);
+  gboolean ret = FALSE;
+
+  self->fd = socket (AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
+  if (self->fd == -1) {
+    int errsv = errno;
+    g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                 "Failed to create netlink socket: %d", errsv);
+    return FALSE;
+  }
+
+  if (fetch_ipv4 (self)) {
+    ret = TRUE;
+    self->ipv4 = g_strdup (self->tmp_addr);
+  }
+  if (fetch_ipv6 (self)) {
+    ret = TRUE;
+    self->ipv6 = g_strdup (self->tmp_addr);
+  }
+
+  return ret;
+}
+
+
+static void
+initable_iface_init (GInitableIface *iface)
+{
+  iface->init = calls_network_watch_initable_init;
+}
+
+
+CallsNetworkWatch *
+calls_network_watch_get_default (void)
+{
+  static CallsNetworkWatch *instance;
+
+  if (instance == NULL) {
+    g_autoptr (GError) error = NULL;
+    instance = g_initable_new (CALLS_TYPE_NETWORK_WATCH, NULL, &error, NULL);
+
+    if (!instance)
+      g_warning ("Network watch could not be initialized: %s", error->message);
+  }
+  return instance;
+}
+
+
+const char *
+calls_network_watch_get_ipv4 (CallsNetworkWatch *self)
+{
+  g_return_val_if_fail (CALLS_IS_NETWORK_WATCH (self), NULL);
+
+  return self->ipv4;
+}
+
+
+const char *
+calls_network_watch_get_ipv6 (CallsNetworkWatch *self)
+{
+  g_return_val_if_fail (CALLS_IS_NETWORK_WATCH (self), NULL);
+
+  return self->ipv6;
+}
diff --git a/src/calls-network-watch.h b/src/calls-network-watch.h
new file mode 100644
index 00000000..5ee40696
--- /dev/null
+++ b/src/calls-network-watch.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2021 Purism SPC
+ *
+ * This file is part of Calls.
+ *
+ * Calls is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Calls is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Calls. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Evangelos Ribeiro Tzaras <evangelos tzaras puri sm>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define CALLS_TYPE_NETWORK_WATCH (calls_network_watch_get_type ())
+
+G_DECLARE_FINAL_TYPE (CallsNetworkWatch, calls_network_watch, CALLS, NETWORK_WATCH, GObject)
+
+CallsNetworkWatch      *calls_network_watch_get_default       (void);
+const char             *calls_network_watch_get_ipv4          (CallsNetworkWatch *self);
+const char             *calls_network_watch_get_ipv6          (CallsNetworkWatch *self);
+
+G_END_DECLS
diff --git a/src/meson.build b/src/meson.build
index dd9aab83..c6ae792f 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -114,6 +114,7 @@ calls_sources = files(['calls-message-source.c', 'calls-message-source.h',
                        'calls-account-row.c', 'calls-account-row.h',
                        'calls-settings.c', 'calls-settings.h',
                        'calls-secret-store.c', 'calls-secret-store.h',
+                       'calls-network-watch.c', 'calls-network-watch.h',
                       ]) + calls_generated_sources
 
 calls_config_data = config_data


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]