2023-10-27 11:42:57 +01: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 08:25:44 +00: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 ( ) ;
}