diff --git a/src/calls-network-watch.c b/src/calls-network-watch.c new file mode 100644 index 0000000..49cee6a --- /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 . + * + * Author: Evangelos Ribeiro Tzaras + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + */ + +#define G_LOG_DOMAIN "CallsNetworkWatch" + +#include "calls-network-watch.h" + +#include + +#include +#include + +/** + * 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 0000000..5ee4069 --- /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 . + * + * Author: Evangelos Ribeiro Tzaras + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + */ + +#pragma once + +#include + +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 dd9aab8..c6ae792 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