| // 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; |
| } |
| |
| /* |
| * 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(); |
| } |