3
0
mirror of https://git.kernel.org/pub/scm/network/wireless/iwd.git synced 2024-11-24 16:49:25 +01:00
iwd/src/ip-pool.c

389 lines
10 KiB
C

/*
*
* Wireless daemon for Linux
*
* Copyright (C) 2021 Intel Corporation. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <linux/rtnetlink.h>
#include <ell/ell.h>
#include "src/util.h"
#include "src/iwd.h"
#include "src/module.h"
#include "src/netdev.h"
#include "src/netconfig.h"
#include "src/ip-pool.h"
struct ip_pool_addr4_record {
uint32_t ifindex;
struct l_rtnl_address *addr;
};
struct ip_pool_addr4_range {
uint32_t start;
uint32_t end;
};
static struct l_queue *used_addr4_list;
static struct l_netlink *rtnl;
static int ip_pool_addr4_range_compare(const void *a, const void *b,
void *user_data)
{
const struct ip_pool_addr4_range *range_a = a;
const struct ip_pool_addr4_range *range_b = b;
return range_a->start > range_b->start ? 1 : -1;
}
/*
* Append any address ranges within an input start/end range which contain
* at least one full subnet and don't intersect with any subnets already in
* use. This may result in the input range being split into multiple ranges
* of different sizes or being skipped altogether.
* All inputs must be rounded to the subnet boundary and the @used queue
* sorted by the subnet start address.
*/
static void ip_pool_append_range(struct l_queue *to,
const struct ip_pool_addr4_range *range,
struct l_queue *used, const char *str)
{
const struct l_queue_entry *entry = l_queue_get_entries(used);
const struct ip_pool_addr4_range *used_range =
entry ? entry->data : NULL;
uint32_t start = range->start;
bool print = true;
while (range->end > start) {
while (used_range && used_range->end <= start) {
entry = entry->next;
used_range = entry ? entry->data : NULL;
}
/* No more used ranges that intersect with @start/@range->end */
if (!used_range || range->end <= used_range->start) {
struct ip_pool_addr4_range *sub =
l_new(struct ip_pool_addr4_range, 1);
sub->start = start;
sub->end = range->end;
l_queue_push_tail(to, sub);
l_queue_insert(used, l_memdup(sub, sizeof(*sub)),
ip_pool_addr4_range_compare, NULL);
return;
}
if (print) {
l_debug("Address spec %s intersects with at least one "
"subnet already in use on the system or "
"specified in the settings", str);
print = false;
}
/* Now we know @used_range is non-NULL and intersects */
if (start < used_range->start) {
struct ip_pool_addr4_range *sub =
l_new(struct ip_pool_addr4_range, 1);
sub->start = start;
sub->end = used_range->start;
l_queue_push_tail(to, sub);
l_queue_insert(used, l_memdup(sub, sizeof(*sub)),
ip_pool_addr4_range_compare, NULL);
}
/* Skip to the start of the next subnet */
start = used_range->end;
}
}
/*
* Select a subnet and a host address from a defined space.
*
* Returns: 0 when an address was selected and written to *out_addr,
* -EEXIST if all available subnet addresses are in use,
* -EINVAL if there was a different error.
*/
int ip_pool_select_addr4(const char **addr_str_list, uint8_t subnet_prefix_len,
struct l_rtnl_address **out_addr)
{
uint32_t total = 0;
uint32_t selected;
unsigned int i;
uint32_t subnet_size = 1 << (32 - subnet_prefix_len);
uint32_t host_mask = subnet_size - 1;
uint32_t subnet_mask = ~host_mask;
uint32_t host_addr = 0;
struct l_queue *ranges = l_queue_new();
struct l_queue *used = l_queue_new();
struct in_addr ia;
const struct l_queue_entry *entry;
int err = -EINVAL;
char ipstr[INET_ADDRSTRLEN];
if (!addr_str_list || !addr_str_list[0])
goto cleanup;
/* Build a sorted list of used/unavailable subnets */
for (entry = l_queue_get_entries(used_addr4_list);
entry; entry = entry->next) {
const struct ip_pool_addr4_record *rec = entry->data;
struct ip_pool_addr4_range *range;
char addr_str[INET_ADDRSTRLEN];
uint8_t used_prefix_len =
l_rtnl_address_get_prefix_length(rec->addr);
uint32_t used_subnet_size;
if (l_rtnl_address_get_family(rec->addr) != AF_INET ||
!l_rtnl_address_get_address(rec->addr,
addr_str) ||
used_prefix_len < 1 ||
inet_pton(AF_INET, addr_str, &ia) != 1)
continue;
used_subnet_size = 1 << (32 - used_prefix_len);
range = l_new(struct ip_pool_addr4_range, 1);
range->start = ntohl(ia.s_addr) & subnet_mask;
range->end = (range->start + used_subnet_size + subnet_size -
1) & subnet_mask;
l_queue_insert(used, range, ip_pool_addr4_range_compare, NULL);
}
/* Build the list of available subnets */
/* Check for the static IP syntax: Address=<IP> */
if (l_strv_length((char **) addr_str_list) == 1 &&
inet_pton(AF_INET, *addr_str_list, &ia) == 1) {
struct ip_pool_addr4_range range;
host_addr = ntohl(ia.s_addr);
range.start = host_addr & subnet_mask;
range.end = range.start + subnet_size;
ip_pool_append_range(ranges, &range, used, *addr_str_list);
goto check_avail;
}
for (i = 0; addr_str_list[i]; i++) {
struct ip_pool_addr4_range range;
uint32_t addr;
uint8_t addr_prefix;
if (!util_ip_prefix_tohl(addr_str_list[i], &addr_prefix, &addr,
NULL, NULL)) {
l_error("Can't parse %s as a subnet address",
addr_str_list[i]);
goto cleanup;
}
if (addr_prefix > subnet_prefix_len) {
l_debug("Address spec %s smaller than requested "
"subnet (prefix len %i)", addr_str_list[i],
subnet_prefix_len);
continue;
}
range.start = addr & subnet_mask;
range.end = range.start + (1 << (32 - addr_prefix));
ip_pool_append_range(ranges, &range, used, addr_str_list[i]);
}
check_avail:
if (l_queue_isempty(ranges)) {
l_error("No IP subnets available for new Access Point after "
"eliminating those already in use on the system");
err = -EEXIST;
goto cleanup;
}
if (host_addr)
goto done;
/* Count available @subnet_prefix_len-sized subnets */
for (entry = l_queue_get_entries(ranges); entry; entry = entry->next) {
struct ip_pool_addr4_range *range = entry->data;
total += (range->end - range->start) >>
(32 - subnet_prefix_len);
}
selected = l_getrandom_uint32() % total;
/* Find the @selected'th @subnet_prefix_len-sized subnet */
for (entry = l_queue_get_entries(ranges);; entry = entry->next) {
struct ip_pool_addr4_range *range = entry->data;
uint32_t count = (range->end - range->start) >>
(32 - subnet_prefix_len);
if (selected < count) {
host_addr = range->start +
(selected << (32 - subnet_prefix_len));
break;
}
selected -= count;
}
if ((host_addr & host_mask) == 0)
host_addr += 1;
done:
err = 0;
ia.s_addr = htonl(host_addr);
if (L_WARN_ON(!inet_ntop(AF_INET, &ia, ipstr, INET_ADDRSTRLEN)))
err = -errno;
else
*out_addr = l_rtnl_address_new(ipstr, subnet_prefix_len);
cleanup:
l_queue_destroy(ranges, l_free);
l_queue_destroy(used, l_free);
return err;
}
static void ip_pool_addr4_record_free(void *data)
{
struct ip_pool_addr4_record *rec = data;
l_rtnl_address_free(rec->addr);
l_free(rec);
}
static bool ip_pool_addr4_match_ifindex(const void *a, const void *b)
{
const struct ip_pool_addr4_record *addr = a;
return addr->ifindex == L_PTR_TO_UINT(b);
}
struct l_rtnl_address *ip_pool_get_addr4(uint32_t ifindex)
{
const struct ip_pool_addr4_record *rec =
l_queue_find(used_addr4_list, ip_pool_addr4_match_ifindex,
L_UINT_TO_PTR(ifindex));
return rec ? l_rtnl_address_clone(rec->addr) : 0;
}
static bool ip_pool_addr4_match_free(void *data, void *user_data)
{
const struct ip_pool_addr4_record *a = data;
const struct ip_pool_addr4_record *b = user_data;
char a_addr_str[INET_ADDRSTRLEN];
char b_addr_str[INET_ADDRSTRLEN];
if (a->ifindex != b->ifindex ||
l_rtnl_address_get_prefix_length(a->addr) !=
l_rtnl_address_get_prefix_length(b->addr))
return false;
if (!l_rtnl_address_get_address(a->addr, a_addr_str) ||
!l_rtnl_address_get_address(b->addr, b_addr_str) ||
strcmp(a_addr_str, b_addr_str))
return false;
ip_pool_addr4_record_free(data);
return true;
}
static void ip_pool_addr_notify(uint16_t type, const void *data, uint32_t len,
void *user_data)
{
const struct ifaddrmsg *ifa = data;
struct l_rtnl_address *addr;
if (ifa->ifa_family != AF_INET || ifa->ifa_prefixlen < 1)
return;
len -= NLMSG_ALIGN(sizeof(struct ifaddrmsg));
addr = l_rtnl_ifaddr_extract(ifa, len);
if (!addr)
return;
if (type == RTM_NEWADDR) {
struct ip_pool_addr4_record *rec;
rec = l_new(struct ip_pool_addr4_record, 1);
rec->ifindex = ifa->ifa_index;
rec->addr = l_steal_ptr(addr);
l_queue_push_tail(used_addr4_list, rec);
} else if (type == RTM_DELADDR) {
struct ip_pool_addr4_record rec;
rec.ifindex = ifa->ifa_index;
rec.addr = addr;
l_queue_foreach_remove(used_addr4_list,
ip_pool_addr4_match_free, &rec);
}
l_rtnl_address_free(addr);
}
static void ip_pool_addr4_dump_cb(int error,
uint16_t type, const void *data,
uint32_t len, void *user_data)
{
if (error) {
l_error("addr4_dump_cb: %s (%i)", strerror(-error), -error);
return;
}
ip_pool_addr_notify(type, data, len, user_data);
}
static int ip_pool_init(void)
{
if (!netconfig_enabled())
return 0;
rtnl = iwd_get_rtnl();
if (!l_netlink_register(rtnl, RTNLGRP_IPV4_IFADDR,
ip_pool_addr_notify, NULL, NULL)) {
l_error("Failed to register for RTNL link notifications");
return -EIO;
}
if (!l_rtnl_ifaddr4_dump(rtnl, ip_pool_addr4_dump_cb, NULL, NULL)) {
l_error("Sending the IPv4 addr dump req failed");
return -EIO;
}
used_addr4_list = l_queue_new();
return 0;
}
static void ip_pool_exit(void)
{
l_queue_destroy(used_addr4_list, ip_pool_addr4_record_free);
used_addr4_list = NULL;
}
IWD_MODULE(ip_pool, ip_pool_init, ip_pool_exit)