2023-10-27 03:42:57 -07:00
|
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
/* Address preferences management
|
|
|
|
*
|
|
|
|
* Copyright (C) 2023 Red Hat, Inc. All Rights Reserved.
|
|
|
|
* Written by David Howells (dhowells@redhat.com)
|
|
|
|
*/
|
|
|
|
|
|
|
|
#define pr_fmt(fmt) KBUILD_MODNAME ": addr_prefs: " fmt
|
|
|
|
#include <linux/slab.h>
|
|
|
|
#include <linux/ctype.h>
|
|
|
|
#include <linux/inet.h>
|
|
|
|
#include <linux/seq_file.h>
|
|
|
|
#include <keys/rxrpc-type.h>
|
|
|
|
#include "internal.h"
|
|
|
|
|
|
|
|
static inline struct afs_net *afs_seq2net_single(struct seq_file *m)
|
|
|
|
{
|
|
|
|
return afs_net(seq_file_single_net(m));
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Split a NUL-terminated string up to the first newline around spaces. The
|
|
|
|
* source string will be modified to have NUL-terminations inserted.
|
|
|
|
*/
|
|
|
|
static int afs_split_string(char **pbuf, char *strv[], unsigned int maxstrv)
|
|
|
|
{
|
|
|
|
unsigned int count = 0;
|
|
|
|
char *p = *pbuf;
|
|
|
|
|
|
|
|
maxstrv--; /* Allow for terminal NULL */
|
|
|
|
for (;;) {
|
|
|
|
/* Skip over spaces */
|
|
|
|
while (isspace(*p)) {
|
|
|
|
if (*p == '\n') {
|
|
|
|
p++;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
p++;
|
|
|
|
}
|
|
|
|
if (!*p)
|
|
|
|
break;
|
|
|
|
|
|
|
|
/* Mark start of word */
|
|
|
|
if (count >= maxstrv) {
|
|
|
|
pr_warn("Too many elements in string\n");
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
strv[count++] = p;
|
|
|
|
|
|
|
|
/* Skip over word */
|
|
|
|
while (!isspace(*p))
|
|
|
|
p++;
|
|
|
|
if (!*p)
|
|
|
|
break;
|
|
|
|
|
|
|
|
/* Mark end of word */
|
|
|
|
if (*p == '\n') {
|
|
|
|
*p++ = 0;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
*p++ = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
*pbuf = p;
|
|
|
|
strv[count] = NULL;
|
|
|
|
return count;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Parse an address with an optional subnet mask.
|
|
|
|
*/
|
|
|
|
static int afs_parse_address(char *p, struct afs_addr_preference *pref)
|
|
|
|
{
|
|
|
|
const char *stop;
|
|
|
|
unsigned long mask, tmp;
|
|
|
|
char *end = p + strlen(p);
|
|
|
|
bool bracket = false;
|
|
|
|
|
|
|
|
if (*p == '[') {
|
|
|
|
p++;
|
|
|
|
bracket = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
#if 0
|
|
|
|
if (*p == '[') {
|
|
|
|
p++;
|
|
|
|
q = memchr(p, ']', end - p);
|
|
|
|
if (!q) {
|
|
|
|
pr_warn("Can't find closing ']'\n");
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
for (q = p; q < end; q++)
|
|
|
|
if (*q == '/')
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
if (in4_pton(p, end - p, (u8 *)&pref->ipv4_addr, -1, &stop)) {
|
|
|
|
pref->family = AF_INET;
|
|
|
|
mask = 32;
|
|
|
|
} else if (in6_pton(p, end - p, (u8 *)&pref->ipv6_addr, -1, &stop)) {
|
|
|
|
pref->family = AF_INET6;
|
|
|
|
mask = 128;
|
|
|
|
} else {
|
|
|
|
pr_warn("Can't determine address family\n");
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
|
|
|
|
p = (char *)stop;
|
|
|
|
if (bracket) {
|
|
|
|
if (*p != ']') {
|
|
|
|
pr_warn("Can't find closing ']'\n");
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
p++;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (*p == '/') {
|
|
|
|
p++;
|
|
|
|
tmp = simple_strtoul(p, &p, 10);
|
|
|
|
if (tmp > mask) {
|
|
|
|
pr_warn("Subnet mask too large\n");
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
if (tmp == 0) {
|
|
|
|
pr_warn("Subnet mask too small\n");
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
mask = tmp;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (*p) {
|
|
|
|
pr_warn("Invalid address\n");
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
|
|
|
|
pref->subnet_mask = mask;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
enum cmp_ret {
|
|
|
|
CONTINUE_SEARCH,
|
|
|
|
INSERT_HERE,
|
|
|
|
EXACT_MATCH,
|
|
|
|
SUBNET_MATCH,
|
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
* See if a candidate address matches a listed address.
|
|
|
|
*/
|
|
|
|
static enum cmp_ret afs_cmp_address_pref(const struct afs_addr_preference *a,
|
|
|
|
const struct afs_addr_preference *b)
|
|
|
|
{
|
|
|
|
int subnet = min(a->subnet_mask, b->subnet_mask);
|
|
|
|
const __be32 *pa, *pb;
|
|
|
|
u32 mask, na, nb;
|
|
|
|
int diff;
|
|
|
|
|
|
|
|
if (a->family != b->family)
|
|
|
|
return INSERT_HERE;
|
|
|
|
|
|
|
|
switch (a->family) {
|
|
|
|
case AF_INET6:
|
|
|
|
pa = a->ipv6_addr.s6_addr32;
|
|
|
|
pb = b->ipv6_addr.s6_addr32;
|
|
|
|
break;
|
|
|
|
case AF_INET:
|
|
|
|
pa = &a->ipv4_addr.s_addr;
|
|
|
|
pb = &b->ipv4_addr.s_addr;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
while (subnet > 32) {
|
|
|
|
diff = ntohl(*pa++) - ntohl(*pb++);
|
|
|
|
if (diff < 0)
|
|
|
|
return INSERT_HERE; /* a<b */
|
|
|
|
if (diff > 0)
|
|
|
|
return CONTINUE_SEARCH; /* a>b */
|
|
|
|
subnet -= 32;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (subnet == 0)
|
|
|
|
return EXACT_MATCH;
|
|
|
|
|
|
|
|
mask = 0xffffffffU << (32 - subnet);
|
|
|
|
na = ntohl(*pa);
|
|
|
|
nb = ntohl(*pb);
|
|
|
|
diff = (na & mask) - (nb & mask);
|
|
|
|
//kdebug("diff %08x %08x %08x %d", na, nb, mask, diff);
|
|
|
|
if (diff < 0)
|
|
|
|
return INSERT_HERE; /* a<b */
|
|
|
|
if (diff > 0)
|
|
|
|
return CONTINUE_SEARCH; /* a>b */
|
|
|
|
if (a->subnet_mask == b->subnet_mask)
|
|
|
|
return EXACT_MATCH;
|
|
|
|
if (a->subnet_mask > b->subnet_mask)
|
|
|
|
return SUBNET_MATCH; /* a binds tighter than b */
|
|
|
|
return CONTINUE_SEARCH; /* b binds tighter than a */
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Insert an address preference.
|
|
|
|
*/
|
|
|
|
static int afs_insert_address_pref(struct afs_addr_preference_list **_preflist,
|
|
|
|
struct afs_addr_preference *pref,
|
|
|
|
int index)
|
|
|
|
{
|
|
|
|
struct afs_addr_preference_list *preflist = *_preflist, *old = preflist;
|
|
|
|
size_t size, max_prefs;
|
|
|
|
|
|
|
|
_enter("{%u/%u/%u},%u", preflist->ipv6_off, preflist->nr, preflist->max_prefs, index);
|
|
|
|
|
|
|
|
if (preflist->nr == 255)
|
|
|
|
return -ENOSPC;
|
|
|
|
if (preflist->nr >= preflist->max_prefs) {
|
|
|
|
max_prefs = preflist->max_prefs + 1;
|
|
|
|
size = struct_size(preflist, prefs, max_prefs);
|
|
|
|
size = roundup_pow_of_two(size);
|
|
|
|
max_prefs = min_t(size_t, (size - sizeof(*preflist)) / sizeof(*pref), 255);
|
|
|
|
preflist = kmalloc(size, GFP_KERNEL);
|
|
|
|
if (!preflist)
|
|
|
|
return -ENOMEM;
|
|
|
|
*preflist = **_preflist;
|
|
|
|
preflist->max_prefs = max_prefs;
|
|
|
|
*_preflist = preflist;
|
|
|
|
|
|
|
|
if (index < preflist->nr)
|
|
|
|
memcpy(preflist->prefs + index + 1, old->prefs + index,
|
|
|
|
sizeof(*pref) * (preflist->nr - index));
|
|
|
|
if (index > 0)
|
|
|
|
memcpy(preflist->prefs, old->prefs, sizeof(*pref) * index);
|
|
|
|
} else {
|
|
|
|
if (index < preflist->nr)
|
|
|
|
memmove(preflist->prefs + index + 1, preflist->prefs + index,
|
|
|
|
sizeof(*pref) * (preflist->nr - index));
|
|
|
|
}
|
|
|
|
|
|
|
|
preflist->prefs[index] = *pref;
|
|
|
|
preflist->nr++;
|
|
|
|
if (pref->family == AF_INET)
|
|
|
|
preflist->ipv6_off++;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Add an address preference.
|
|
|
|
* echo "add <proto> <IP>[/<mask>] <prior>" >/proc/fs/afs/addr_prefs
|
|
|
|
*/
|
|
|
|
static int afs_add_address_pref(struct afs_net *net, struct afs_addr_preference_list **_preflist,
|
|
|
|
int argc, char **argv)
|
|
|
|
{
|
|
|
|
struct afs_addr_preference_list *preflist = *_preflist;
|
|
|
|
struct afs_addr_preference pref;
|
|
|
|
enum cmp_ret cmp;
|
|
|
|
int ret, i, stop;
|
|
|
|
|
|
|
|
if (argc != 3) {
|
|
|
|
pr_warn("Wrong number of params\n");
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (strcmp(argv[0], "udp") != 0) {
|
|
|
|
pr_warn("Unsupported protocol\n");
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
|
|
|
|
ret = afs_parse_address(argv[1], &pref);
|
|
|
|
if (ret < 0)
|
|
|
|
return ret;
|
|
|
|
|
|
|
|
ret = kstrtou16(argv[2], 10, &pref.prio);
|
|
|
|
if (ret < 0) {
|
|
|
|
pr_warn("Invalid priority\n");
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (pref.family == AF_INET) {
|
|
|
|
i = 0;
|
|
|
|
stop = preflist->ipv6_off;
|
|
|
|
} else {
|
|
|
|
i = preflist->ipv6_off;
|
|
|
|
stop = preflist->nr;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (; i < stop; i++) {
|
|
|
|
cmp = afs_cmp_address_pref(&pref, &preflist->prefs[i]);
|
|
|
|
switch (cmp) {
|
|
|
|
case CONTINUE_SEARCH:
|
|
|
|
continue;
|
|
|
|
case INSERT_HERE:
|
|
|
|
case SUBNET_MATCH:
|
|
|
|
return afs_insert_address_pref(_preflist, &pref, i);
|
|
|
|
case EXACT_MATCH:
|
|
|
|
preflist->prefs[i].prio = pref.prio;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return afs_insert_address_pref(_preflist, &pref, i);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Delete an address preference.
|
|
|
|
*/
|
|
|
|
static int afs_delete_address_pref(struct afs_addr_preference_list **_preflist,
|
|
|
|
int index)
|
|
|
|
{
|
|
|
|
struct afs_addr_preference_list *preflist = *_preflist;
|
|
|
|
|
|
|
|
_enter("{%u/%u/%u},%u", preflist->ipv6_off, preflist->nr, preflist->max_prefs, index);
|
|
|
|
|
|
|
|
if (preflist->nr == 0)
|
|
|
|
return -ENOENT;
|
|
|
|
|
|
|
|
if (index < preflist->nr - 1)
|
|
|
|
memmove(preflist->prefs + index, preflist->prefs + index + 1,
|
|
|
|
sizeof(preflist->prefs[0]) * (preflist->nr - index - 1));
|
|
|
|
|
|
|
|
if (index < preflist->ipv6_off)
|
|
|
|
preflist->ipv6_off--;
|
|
|
|
preflist->nr--;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Delete an address preference.
|
|
|
|
* echo "del <proto> <IP>[/<mask>]" >/proc/fs/afs/addr_prefs
|
|
|
|
*/
|
|
|
|
static int afs_del_address_pref(struct afs_net *net, struct afs_addr_preference_list **_preflist,
|
|
|
|
int argc, char **argv)
|
|
|
|
{
|
|
|
|
struct afs_addr_preference_list *preflist = *_preflist;
|
|
|
|
struct afs_addr_preference pref;
|
|
|
|
enum cmp_ret cmp;
|
|
|
|
int ret, i, stop;
|
|
|
|
|
|
|
|
if (argc != 2) {
|
|
|
|
pr_warn("Wrong number of params\n");
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (strcmp(argv[0], "udp") != 0) {
|
|
|
|
pr_warn("Unsupported protocol\n");
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
|
|
|
|
ret = afs_parse_address(argv[1], &pref);
|
|
|
|
if (ret < 0)
|
|
|
|
return ret;
|
|
|
|
|
|
|
|
if (pref.family == AF_INET) {
|
|
|
|
i = 0;
|
|
|
|
stop = preflist->ipv6_off;
|
|
|
|
} else {
|
|
|
|
i = preflist->ipv6_off;
|
|
|
|
stop = preflist->nr;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (; i < stop; i++) {
|
|
|
|
cmp = afs_cmp_address_pref(&pref, &preflist->prefs[i]);
|
|
|
|
switch (cmp) {
|
|
|
|
case CONTINUE_SEARCH:
|
|
|
|
continue;
|
|
|
|
case INSERT_HERE:
|
|
|
|
case SUBNET_MATCH:
|
|
|
|
return 0;
|
|
|
|
case EXACT_MATCH:
|
|
|
|
return afs_delete_address_pref(_preflist, i);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return -ENOANO;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Handle writes to /proc/fs/afs/addr_prefs
|
|
|
|
*/
|
|
|
|
int afs_proc_addr_prefs_write(struct file *file, char *buf, size_t size)
|
|
|
|
{
|
|
|
|
struct afs_addr_preference_list *preflist, *old;
|
|
|
|
struct seq_file *m = file->private_data;
|
|
|
|
struct afs_net *net = afs_seq2net_single(m);
|
|
|
|
size_t psize;
|
|
|
|
char *argv[5];
|
|
|
|
int ret, argc, max_prefs;
|
|
|
|
|
|
|
|
inode_lock(file_inode(file));
|
|
|
|
|
|
|
|
/* Allocate a candidate new list and initialise it from the old. */
|
|
|
|
old = rcu_dereference_protected(net->address_prefs,
|
|
|
|
lockdep_is_held(&file_inode(file)->i_rwsem));
|
|
|
|
|
|
|
|
if (old)
|
|
|
|
max_prefs = old->nr + 1;
|
|
|
|
else
|
|
|
|
max_prefs = 1;
|
|
|
|
|
|
|
|
psize = struct_size(old, prefs, max_prefs);
|
|
|
|
psize = roundup_pow_of_two(psize);
|
|
|
|
max_prefs = min_t(size_t, (psize - sizeof(*old)) / sizeof(old->prefs[0]), 255);
|
|
|
|
|
|
|
|
ret = -ENOMEM;
|
|
|
|
preflist = kmalloc(struct_size(preflist, prefs, max_prefs), GFP_KERNEL);
|
|
|
|
if (!preflist)
|
|
|
|
goto done;
|
|
|
|
|
|
|
|
if (old)
|
|
|
|
memcpy(preflist, old, struct_size(preflist, prefs, old->nr));
|
|
|
|
else
|
|
|
|
memset(preflist, 0, sizeof(*preflist));
|
|
|
|
preflist->max_prefs = max_prefs;
|
|
|
|
|
|
|
|
do {
|
|
|
|
argc = afs_split_string(&buf, argv, ARRAY_SIZE(argv));
|
|
|
|
if (argc < 0)
|
|
|
|
return argc;
|
|
|
|
if (argc < 2)
|
|
|
|
goto inval;
|
|
|
|
|
|
|
|
if (strcmp(argv[0], "add") == 0)
|
|
|
|
ret = afs_add_address_pref(net, &preflist, argc - 1, argv + 1);
|
|
|
|
else if (strcmp(argv[0], "del") == 0)
|
|
|
|
ret = afs_del_address_pref(net, &preflist, argc - 1, argv + 1);
|
|
|
|
else
|
|
|
|
goto inval;
|
|
|
|
if (ret < 0)
|
|
|
|
goto done;
|
|
|
|
} while (*buf);
|
|
|
|
|
|
|
|
preflist->version++;
|
|
|
|
rcu_assign_pointer(net->address_prefs, preflist);
|
|
|
|
/* Store prefs before version */
|
|
|
|
smp_store_release(&net->address_pref_version, preflist->version);
|
|
|
|
kfree_rcu(old, rcu);
|
|
|
|
preflist = NULL;
|
|
|
|
ret = 0;
|
|
|
|
|
|
|
|
done:
|
|
|
|
kfree(preflist);
|
|
|
|
inode_unlock(file_inode(file));
|
|
|
|
_leave(" = %d", ret);
|
|
|
|
return ret;
|
|
|
|
|
|
|
|
inval:
|
|
|
|
pr_warn("Invalid Command\n");
|
|
|
|
ret = -EINVAL;
|
|
|
|
goto done;
|
|
|
|
}
|
2023-10-30 01:25:44 -07:00
|
|
|
|
|
|
|
/*
|
|
|
|
* Mark the priorities on an address list if the address preferences table has
|
|
|
|
* changed. The caller must hold the RCU read lock.
|
|
|
|
*/
|
|
|
|
void afs_get_address_preferences_rcu(struct afs_net *net, struct afs_addr_list *alist)
|
|
|
|
{
|
|
|
|
const struct afs_addr_preference_list *preflist =
|
|
|
|
rcu_dereference(net->address_prefs);
|
|
|
|
const struct sockaddr_in6 *sin6;
|
|
|
|
const struct sockaddr_in *sin;
|
|
|
|
const struct sockaddr *sa;
|
|
|
|
struct afs_addr_preference test;
|
|
|
|
enum cmp_ret cmp;
|
|
|
|
int i, j;
|
|
|
|
|
|
|
|
if (!preflist || !preflist->nr || !alist->nr_addrs ||
|
|
|
|
smp_load_acquire(&alist->addr_pref_version) == preflist->version)
|
|
|
|
return;
|
|
|
|
|
|
|
|
test.family = AF_INET;
|
|
|
|
test.subnet_mask = 32;
|
|
|
|
test.prio = 0;
|
|
|
|
for (i = 0; i < alist->nr_ipv4; i++) {
|
|
|
|
sa = rxrpc_kernel_remote_addr(alist->addrs[i].peer);
|
|
|
|
sin = (const struct sockaddr_in *)sa;
|
|
|
|
test.ipv4_addr = sin->sin_addr;
|
|
|
|
for (j = 0; j < preflist->ipv6_off; j++) {
|
|
|
|
cmp = afs_cmp_address_pref(&test, &preflist->prefs[j]);
|
|
|
|
switch (cmp) {
|
|
|
|
case CONTINUE_SEARCH:
|
|
|
|
continue;
|
|
|
|
case INSERT_HERE:
|
|
|
|
break;
|
|
|
|
case EXACT_MATCH:
|
|
|
|
case SUBNET_MATCH:
|
|
|
|
WRITE_ONCE(alist->addrs[i].prio, preflist->prefs[j].prio);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
test.family = AF_INET6;
|
|
|
|
test.subnet_mask = 128;
|
|
|
|
test.prio = 0;
|
|
|
|
for (; i < alist->nr_addrs; i++) {
|
|
|
|
sa = rxrpc_kernel_remote_addr(alist->addrs[i].peer);
|
|
|
|
sin6 = (const struct sockaddr_in6 *)sa;
|
|
|
|
test.ipv6_addr = sin6->sin6_addr;
|
|
|
|
for (j = preflist->ipv6_off; j < preflist->nr; j++) {
|
|
|
|
cmp = afs_cmp_address_pref(&test, &preflist->prefs[j]);
|
|
|
|
switch (cmp) {
|
|
|
|
case CONTINUE_SEARCH:
|
|
|
|
continue;
|
|
|
|
case INSERT_HERE:
|
|
|
|
break;
|
|
|
|
case EXACT_MATCH:
|
|
|
|
case SUBNET_MATCH:
|
|
|
|
WRITE_ONCE(alist->addrs[i].prio, preflist->prefs[j].prio);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
smp_store_release(&alist->addr_pref_version, preflist->version);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Mark the priorities on an address list if the address preferences table has
|
|
|
|
* changed. Avoid taking the RCU read lock if we can.
|
|
|
|
*/
|
|
|
|
void afs_get_address_preferences(struct afs_net *net, struct afs_addr_list *alist)
|
|
|
|
{
|
|
|
|
if (!net->address_prefs ||
|
|
|
|
/* Load version before prefs */
|
|
|
|
smp_load_acquire(&net->address_pref_version) == alist->addr_pref_version)
|
|
|
|
return;
|
|
|
|
|
|
|
|
rcu_read_lock();
|
|
|
|
afs_get_address_preferences_rcu(net, alist);
|
|
|
|
rcu_read_unlock();
|
|
|
|
}
|