// 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 #include #include #include #include #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 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 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 [/] " >/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 [/]" >/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(); }