| /* |
| * oFono - Open Source Telephony |
| * Copyright (C) 2024 Cruise, LLC |
| * |
| * SPDX-License-Identifier: LGPL-2.1-or-later |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include <config.h> |
| #endif |
| |
| #include <stddef.h> |
| #include <errno.h> |
| #include <unistd.h> |
| #include <stdio.h> |
| #include <linux/rtnetlink.h> |
| #include <net/if.h> |
| #include <linux/if_link.h> |
| #include <linux/if_arp.h> |
| |
| #include <ell/ell.h> |
| |
| #include "ofono.h" |
| #include "rmnet.h" |
| |
| #define RMNET_TYPE "rmnet" |
| #define MAX_MUX_IDS 254U |
| #define DEFAULT_MTU 1400U |
| |
| /* |
| * These were added in 5.14 so define them here if the toolchain's kernel headers are old. |
| */ |
| #ifndef RMNET_FLAGS_INGRESS_MAP_CKSUMV5 |
| #define RMNET_FLAGS_INGRESS_MAP_CKSUMV5 (1U << 4) |
| #endif |
| #ifndef RMNET_FLAGS_EGRESS_MAP_CKSUMV5 |
| #define RMNET_FLAGS_EGRESS_MAP_CKSUMV5 (1U << 5) |
| #endif |
| |
| struct rmnet_request { |
| uint32_t parent_ifindex; |
| rmnet_new_interfaces_func_t new_cb; |
| void *user_data; |
| rmnet_destroy_func_t destroy; |
| int id; |
| bool canceled; |
| uint32_t netlink_id; |
| uint16_t request_type; |
| uint8_t current; |
| uint8_t n_interfaces; |
| struct rmnet_ifinfo infos[]; |
| }; |
| |
| static struct l_netlink *rtnl; |
| static uint32_t dump_id; |
| static uint32_t link_notify_id; |
| static struct l_uintset *mux_ids; |
| struct l_queue *request_q; |
| static int next_request_id = 1; |
| |
| static void rmnet_request_free(struct rmnet_request *req) |
| { |
| if (req->destroy) |
| req->destroy(req->user_data); |
| |
| l_free(req); |
| } |
| |
| static bool rmnet_request_id_matches(const void *a, const void *b) |
| { |
| const struct rmnet_request *req = a; |
| int id = L_PTR_TO_INT(b); |
| |
| return req->id == id; |
| } |
| |
| static struct rmnet_request *__rmnet_del_request_new(unsigned int n_interfaces, |
| const struct rmnet_ifinfo *interfaces) |
| { |
| struct rmnet_request *req; |
| |
| req = l_malloc(sizeof(struct rmnet_request) + |
| sizeof(struct rmnet_ifinfo) * n_interfaces); |
| memset(req, 0, sizeof(struct rmnet_request)); |
| req->request_type = RTM_DELLINK; |
| req->n_interfaces = n_interfaces; |
| memcpy(req->infos, interfaces, |
| sizeof(struct rmnet_ifinfo) * n_interfaces); |
| |
| return req; |
| } |
| |
| static struct rmnet_request *__rmnet_cancel_request(void) |
| { |
| struct rmnet_request *req = l_queue_pop_head(request_q); |
| |
| if (req->current) { |
| struct rmnet_request *del_req = |
| __rmnet_del_request_new(req->current, req->infos); |
| l_queue_push_head(request_q, del_req); |
| } |
| |
| return req; |
| } |
| |
| static int rmnet_link_del(uint32_t ifindex, l_netlink_command_func_t cb, |
| void *userdata, |
| l_netlink_destroy_func_t destroy, |
| uint32_t *out_command_id) |
| { |
| struct l_netlink_message *nlm = |
| l_netlink_message_new(RTM_DELLINK, 0); |
| struct ifinfomsg ifi; |
| uint32_t id; |
| |
| memset(&ifi, 0, sizeof(ifi)); |
| ifi.ifi_family = AF_UNSPEC; |
| ifi.ifi_index = ifindex; |
| |
| l_netlink_message_add_header(nlm, &ifi, sizeof(ifi)); |
| |
| id = l_netlink_send(rtnl, nlm, cb, userdata, destroy); |
| if (!id) { |
| l_netlink_message_unref(nlm); |
| return -EIO; |
| } |
| |
| if (out_command_id) |
| *out_command_id = id; |
| |
| return 0; |
| } |
| |
| static int rmnet_link_new(uint32_t parent_ifindex, uint8_t mux_id, |
| const char ifname[static IF_NAMESIZE], |
| l_netlink_command_func_t cb, |
| void *userdata, |
| l_netlink_destroy_func_t destroy, |
| uint32_t *out_command_id) |
| { |
| struct ifinfomsg ifi; |
| struct l_netlink_message *nlm = |
| l_netlink_message_new(RTM_NEWLINK, NLM_F_EXCL | NLM_F_CREATE); |
| struct ifla_rmnet_flags flags; |
| uint32_t id; |
| |
| memset(&ifi, 0, sizeof(ifi)); |
| ifi.ifi_family = AF_UNSPEC; |
| ifi.ifi_type = ARPHRD_RAWIP; |
| ifi.ifi_flags = 0; |
| ifi.ifi_change = 0xFFFFFFFF; |
| |
| l_netlink_message_add_header(nlm, &ifi, sizeof(ifi)); |
| l_netlink_message_append_u32(nlm, IFLA_LINK, parent_ifindex); |
| l_netlink_message_append_string(nlm, IFLA_IFNAME, ifname); |
| |
| l_netlink_message_enter_nested(nlm, IFLA_LINKINFO); |
| l_netlink_message_append_string(nlm, IFLA_INFO_KIND, RMNET_TYPE); |
| l_netlink_message_enter_nested(nlm, IFLA_INFO_DATA); |
| l_netlink_message_append_u16(nlm, IFLA_RMNET_MUX_ID, mux_id); |
| flags.flags = RMNET_FLAGS_INGRESS_DEAGGREGATION | |
| RMNET_FLAGS_INGRESS_MAP_CKSUMV5 | |
| RMNET_FLAGS_EGRESS_MAP_CKSUMV5; |
| flags.mask = RMNET_FLAGS_EGRESS_MAP_CKSUMV4 | |
| RMNET_FLAGS_INGRESS_MAP_CKSUMV4 | |
| RMNET_FLAGS_EGRESS_MAP_CKSUMV5 | |
| RMNET_FLAGS_INGRESS_MAP_CKSUMV5 | |
| RMNET_FLAGS_INGRESS_DEAGGREGATION; |
| l_netlink_message_append(nlm, IFLA_RMNET_FLAGS, &flags, sizeof(flags)); |
| l_netlink_message_leave_nested(nlm); |
| l_netlink_message_leave_nested(nlm); |
| |
| id = l_netlink_send(rtnl, nlm, cb, userdata, destroy); |
| if (!id) { |
| l_netlink_message_unref(nlm); |
| return -EIO; |
| } |
| |
| if (out_command_id) |
| *out_command_id = id; |
| |
| return 0; |
| } |
| |
| static void rmnet_start_next_request(void); |
| |
| static void rmnet_del_link_cb(int error, uint16_t type, const void *data, |
| uint32_t len, void *user_data) |
| { |
| struct rmnet_request *req = l_queue_peek_head(request_q); |
| |
| DBG("DELLINK %u (%u/%u) complete, error: %d", |
| req->netlink_id, req->current, req->n_interfaces, error); |
| |
| req->netlink_id = 0; |
| req->current += 1; |
| |
| if (req->current < req->n_interfaces) |
| goto next_request; |
| |
| l_queue_pop_head(request_q); |
| rmnet_request_free(req); |
| |
| next_request: |
| if (l_queue_length(request_q) > 0) |
| rmnet_start_next_request(); |
| } |
| |
| static void rmnet_set_mtu_cb(int error, uint16_t type, const void *data, |
| uint32_t len, void *user_data) |
| { |
| struct rmnet_request *req = l_queue_peek_head(request_q); |
| |
| DBG("%u (%u/%u) complete, error: %d", |
| req->netlink_id, req->current, req->n_interfaces, error); |
| |
| req->netlink_id = 0; |
| |
| if (error || req->canceled) { |
| __rmnet_cancel_request(); |
| req->n_interfaces = 0; |
| } else { |
| if (req->current < req->n_interfaces) |
| goto next_request; |
| |
| l_queue_pop_head(request_q); |
| } |
| |
| if (req->new_cb) |
| req->new_cb(error, req->n_interfaces, |
| req->n_interfaces ? req->infos : NULL, |
| req->user_data); |
| |
| rmnet_request_free(req); |
| next_request: |
| if (l_queue_length(request_q) > 0) |
| rmnet_start_next_request(); |
| } |
| |
| static void rmnet_new_link_cb(int error, uint16_t type, const void *data, |
| uint32_t len, void *user_data) |
| { |
| struct rmnet_request *req = l_queue_peek_head(request_q); |
| uint32_t ifindex = req->infos[req->current].ifindex; |
| |
| DBG("NEWLINK %u (%u/%u) complete, error: %d", |
| req->netlink_id, req->current + 1, req->n_interfaces, error); |
| |
| req->netlink_id = 0; |
| |
| if (!error) |
| req->current += 1; |
| |
| if (error || req->canceled) |
| goto error; |
| |
| /* |
| * Unfortunately the kernel does not take IFLA_MTU into account |
| * when creating new RMNet links, so this must be done as a separate |
| * step. Hopefully this is fixed one day |
| */ |
| error = -EIO; |
| req->netlink_id = l_rtnl_link_set_mtu(rtnl, ifindex, DEFAULT_MTU, |
| rmnet_set_mtu_cb, NULL, NULL); |
| if (req->netlink_id) { |
| DBG("Set MTU: interface: %u/%u, request: %u", |
| req->current, req->n_interfaces, req->netlink_id); |
| return; |
| } |
| error: |
| __rmnet_cancel_request(); |
| req->n_interfaces = 0; |
| |
| if (req->new_cb) |
| req->new_cb(error, req->n_interfaces, |
| req->n_interfaces ? req->infos : NULL, |
| req->user_data); |
| |
| rmnet_request_free(req); |
| } |
| |
| static void rmnet_start_next_request(void) |
| { |
| struct rmnet_request *req = l_queue_peek_head(request_q); |
| uint32_t mux_id; |
| struct rmnet_ifinfo *info; |
| |
| if (!req) |
| return; |
| |
| if (req->request_type == RTM_DELLINK) { |
| uint32_t ifindex = req->infos[req->current].ifindex; |
| |
| L_WARN_ON(rmnet_link_del(ifindex, rmnet_del_link_cb, NULL, NULL, |
| &req->netlink_id) < 0); |
| DBG("Start DELLINK: ifindex: %u, interface: %u/%u, request: %u", |
| ifindex, req->current, |
| req->n_interfaces, req->netlink_id); |
| return; |
| } |
| |
| info = req->infos + req->current; |
| mux_id = l_uintset_find_unused_min(mux_ids); |
| info->mux_id = mux_id; |
| sprintf(info->ifname, RMNET_TYPE"%u", mux_id - 1); |
| |
| L_WARN_ON(rmnet_link_new(req->parent_ifindex, mux_id, info->ifname, |
| rmnet_new_link_cb, NULL, NULL, |
| &req->netlink_id) < 0); |
| |
| DBG("Start NEWLINK: parent: %u, interface: %u/%u, request: %u", |
| req->parent_ifindex, req->current + 1, |
| req->n_interfaces, req->netlink_id); |
| } |
| |
| int rmnet_get_interfaces(uint32_t parent_ifindex, unsigned int n_interfaces, |
| rmnet_new_interfaces_func_t cb, |
| void *user_data, rmnet_destroy_func_t destroy) |
| { |
| struct rmnet_request *req; |
| |
| if (!n_interfaces || n_interfaces > MAX_MUX_IDS) |
| return -EINVAL; |
| |
| if (l_uintset_size(mux_ids) > MAX_MUX_IDS - n_interfaces) |
| return -ENOSPC; |
| |
| req = l_malloc(sizeof(struct rmnet_request) + |
| sizeof(struct rmnet_ifinfo) * n_interfaces); |
| req->parent_ifindex = parent_ifindex; |
| req->new_cb = cb; |
| req->user_data = user_data; |
| req->destroy = destroy; |
| req->id = next_request_id++; |
| req->canceled = false; |
| req->request_type = RTM_NEWLINK; |
| req->netlink_id = 0; |
| req->current = 0; |
| req->n_interfaces = n_interfaces; |
| memset(req->infos, 0, sizeof(struct rmnet_ifinfo) * n_interfaces); |
| |
| if (next_request_id < 0) |
| next_request_id = 1; |
| |
| l_queue_push_tail(request_q, req); |
| |
| if (l_queue_length(request_q) == 1 && !dump_id) |
| rmnet_start_next_request(); |
| |
| return req->id; |
| } |
| |
| int rmnet_del_interfaces(unsigned int n_interfaces, |
| const struct rmnet_ifinfo *interfaces) |
| { |
| struct rmnet_request *req; |
| |
| if (!n_interfaces || n_interfaces > MAX_MUX_IDS) |
| return -EINVAL; |
| |
| req = __rmnet_del_request_new(n_interfaces, interfaces); |
| l_queue_push_tail(request_q, req); |
| |
| if (l_queue_length(request_q) == 1 && !dump_id) |
| rmnet_start_next_request(); |
| |
| return 0; |
| } |
| |
| int rmnet_cancel(int id) |
| { |
| struct rmnet_request *req; |
| |
| req = l_queue_peek_head(request_q); |
| if (!req) |
| return -ENOENT; |
| |
| /* Simple Case: Request not yet started (not queue head) */ |
| if (req->id != id) { |
| req = l_queue_remove_if(request_q, rmnet_request_id_matches, |
| L_INT_TO_PTR(id)); |
| if (!req) |
| return -ENOENT; |
| |
| DBG("Removing non-head of queue request %d", id); |
| rmnet_request_free(req); |
| return 0; |
| } |
| |
| /* Harder Case: In progress, but the next request not in flight */ |
| if (!l_netlink_request_sent(rtnl, req->netlink_id)) { |
| DBG("Removing in-progress request (not in flight) %d", id); |
| req = __rmnet_cancel_request(); |
| l_netlink_cancel(rtnl, req->netlink_id); |
| rmnet_request_free(req); |
| |
| if (l_queue_length(request_q)) |
| rmnet_start_next_request(); |
| |
| return 0; |
| } |
| |
| /* |
| * Hardest Case: In progress, next request in flight |
| * We have to wait until the next callback since the ifindex won't be |
| * known until then. |
| */ |
| if (req->destroy) |
| req->destroy(req->user_data); |
| |
| req->new_cb = NULL; |
| req->destroy = NULL; |
| req->user_data = NULL; |
| |
| DBG("Setting canceled on in-progress request %d", id); |
| req->canceled = true; |
| |
| return 0; |
| } |
| |
| static int rmnet_parse_info_data(struct l_netlink_attr *linkinfo, |
| uint16_t *out_mux_id) |
| { |
| struct l_netlink_attr info_data; |
| bool have_info_data = false; |
| uint16_t rta_type; |
| uint16_t rta_len; |
| const void *rta_data; |
| uint16_t mux_id; |
| |
| while (!l_netlink_attr_next(linkinfo, &rta_type, &rta_len, &rta_data)) { |
| switch (rta_type) { |
| case IFLA_INFO_KIND: |
| if (strncmp(rta_data, RMNET_TYPE, rta_len)) |
| return -EPROTOTYPE; |
| |
| break; |
| case IFLA_INFO_DATA: |
| if (l_netlink_attr_recurse(linkinfo, &info_data) < 0) |
| return -EBADMSG; |
| |
| have_info_data = true; |
| break; |
| } |
| } |
| |
| if (!have_info_data) |
| return -ENOENT; |
| |
| while (!l_netlink_attr_next(&info_data, |
| &rta_type, &rta_len, &rta_data)) { |
| if (rta_type != IFLA_RMNET_MUX_ID) |
| continue; |
| |
| if (rta_len != sizeof(uint16_t)) |
| return -EBADMSG; |
| |
| mux_id = l_get_u16(rta_data); |
| if (mux_id > MAX_MUX_IDS || !mux_id) |
| return -ERANGE; |
| |
| if (out_mux_id) |
| *out_mux_id = mux_id; |
| |
| return 0; |
| } |
| |
| return -ENOENT; |
| } |
| |
| static int rmnet_parse_link(const void *data, uint32_t len, |
| char *out_ifname, |
| uint32_t *out_ifindex, |
| uint16_t *out_mux_id) |
| { |
| const struct ifaddrmsg *ifa = data; |
| struct l_netlink_attr attr; |
| struct l_netlink_attr linkinfo; |
| bool have_linkinfo = false; |
| const char *ifname = NULL; |
| uint16_t ifnamelen = 0; |
| uint32_t mtu = 0; |
| uint16_t rta_type; |
| uint16_t rta_len; |
| const void *rta_data; |
| int r; |
| |
| if (l_netlink_attr_init(&attr, sizeof(struct ifinfomsg), |
| data, len) < 0) |
| return -EBADMSG; |
| |
| while (!l_netlink_attr_next(&attr, &rta_type, &rta_len, &rta_data)) { |
| switch (rta_type) { |
| case IFLA_IFNAME: |
| ifname = rta_data; |
| ifnamelen = rta_len; |
| break; |
| case IFLA_LINKINFO: |
| if (l_netlink_attr_recurse(&attr, &linkinfo) < 0) |
| return -EBADMSG; |
| |
| have_linkinfo = true; |
| break; |
| case IFLA_MTU: |
| if (rta_len != sizeof(uint32_t)) |
| return -EBADMSG; |
| |
| mtu = l_get_u32(rta_data); |
| break; |
| } |
| } |
| |
| if (!have_linkinfo || !ifname || !ifnamelen || !mtu) |
| return -ENOENT; |
| |
| r = rmnet_parse_info_data(&linkinfo, out_mux_id); |
| if (r < 0) |
| return r; |
| |
| if (out_ifname) |
| l_strlcpy(out_ifname, ifname, MIN(ifnamelen + 1, IF_NAMESIZE)); |
| |
| if (out_ifindex) |
| *out_ifindex = ifa->ifa_index; |
| |
| return 0; |
| } |
| |
| static void rmnet_link_dump_destroy(void *user_data) |
| { |
| dump_id = 0; |
| |
| if (l_queue_length(request_q) > 0) |
| rmnet_start_next_request(); |
| } |
| |
| static void rmnet_link_dump_cb(int error, |
| uint16_t type, const void *data, |
| uint32_t len, void *user_data) |
| { |
| struct rmnet_ifinfo info; |
| struct rmnet_request *req; |
| |
| /* Check conditions that can't happen on a dump */ |
| if (error || type != RTM_NEWLINK) |
| return; |
| |
| if (rmnet_parse_link(data, len, |
| info.ifname, &info.ifindex, &info.mux_id) < 0) |
| return; |
| |
| DBG("Removing existing rmnet link: %s(%u) mux_id: %u", |
| info.ifname, info.ifindex, info.mux_id); |
| l_uintset_put(mux_ids, info.mux_id); |
| |
| req = __rmnet_del_request_new(1, &info); |
| l_queue_push_tail(request_q, req); |
| } |
| |
| static int rmnet_link_dump(void) |
| { |
| struct ifinfomsg ifi; |
| struct l_netlink_message *nlm = |
| l_netlink_message_new_sized(RTM_GETLINK, NLM_F_DUMP, |
| sizeof(ifi)); |
| |
| memset(&ifi, 0, sizeof(ifi)); |
| l_netlink_message_add_header(nlm, &ifi, sizeof(ifi)); |
| |
| l_netlink_message_enter_nested(nlm, IFLA_LINKINFO); |
| l_netlink_message_append_string(nlm, IFLA_INFO_KIND, RMNET_TYPE); |
| l_netlink_message_leave_nested(nlm); |
| |
| dump_id = l_netlink_send(rtnl, nlm, rmnet_link_dump_cb, NULL, |
| rmnet_link_dump_destroy); |
| if (dump_id > 0) |
| return 0; |
| |
| l_netlink_message_unref(nlm); |
| return -EIO; |
| } |
| |
| /* For NEW_LINK requests, the ifindex comes in the multicast message */ |
| static void update_new_link_ifindex(uint16_t mux_id, |
| const char ifname[static IF_NAMESIZE], |
| uint32_t ifindex) |
| { |
| struct rmnet_request *req; |
| struct rmnet_ifinfo *info; |
| unsigned int i; |
| |
| req = l_queue_peek_head(request_q); |
| if (!req || req->request_type != RTM_NEWLINK) |
| return; |
| |
| for (i = 0; i < req->n_interfaces; i++) { |
| info = req->infos + i; |
| |
| if (info->mux_id != mux_id) |
| continue; |
| |
| if (strcmp(info->ifname, ifname)) |
| continue; |
| |
| info->ifindex = ifindex; |
| } |
| } |
| |
| static void rmnet_link_notification(uint16_t type, const void *data, |
| uint32_t len, void *user_data) |
| { |
| char ifname[IF_NAMESIZE]; |
| uint16_t mux_id; |
| uint32_t ifindex; |
| |
| if (type != RTM_NEWLINK && type != RTM_DELLINK) |
| return; |
| |
| if (rmnet_parse_link(data, len, ifname, &ifindex, &mux_id) < 0) |
| return; |
| |
| if (type == RTM_NEWLINK) { |
| l_uintset_put(mux_ids, mux_id); |
| update_new_link_ifindex(mux_id, ifname, ifindex); |
| } else |
| l_uintset_take(mux_ids, mux_id); |
| |
| DBG("link_notification: %s(%u) with mux_id: %u", |
| ifname, ifindex, mux_id); |
| } |
| |
| static int rmnet_init(void) |
| { |
| int r; |
| |
| DBG(""); |
| |
| rtnl = l_netlink_new(NETLINK_ROUTE); |
| if (!rtnl) |
| return -EIO; |
| |
| r = rmnet_link_dump(); |
| if (r < 0) |
| goto dump_failed; |
| |
| link_notify_id = l_netlink_register(rtnl, RTNLGRP_LINK, |
| rmnet_link_notification, NULL, NULL); |
| mux_ids = l_uintset_new_from_range(1, MAX_MUX_IDS); |
| request_q = l_queue_new(); |
| |
| return 0; |
| dump_failed: |
| l_netlink_destroy(rtnl); |
| return r; |
| } |
| |
| static void rmnet_exit(void) |
| { |
| l_queue_destroy(request_q, (l_queue_destroy_func_t) rmnet_request_free); |
| l_uintset_free(mux_ids); |
| l_netlink_unregister(rtnl, link_notify_id); |
| l_netlink_destroy(rtnl); |
| } |
| |
| OFONO_MODULE(rmnet, rmnet_init, rmnet_exit) |