1
0
mirror of https://git.libssh.org/projects/libssh.git synced 2025-12-08 03:42:12 +03:00

feature: Add match_localnetwork predicate and its feature

Signed-off-by: Francesco Rollo <eferollo@gmail.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
Reviewed-by: Eshan Kelkar <eshankelkar@galorithm.com>
This commit is contained in:
Francesco Rollo
2024-05-29 18:01:01 +02:00
committed by Sahana Prasad
parent c93a730bc1
commit e90df71955
5 changed files with 535 additions and 17 deletions

View File

@@ -33,7 +33,7 @@
# endif /* _MSC_VER */
#else
# include <sys/types.h>
#include <sys/types.h>
#endif /* _WIN32 */
#ifdef __cplusplus

View File

@@ -330,6 +330,11 @@ int decompress_buffer(ssh_session session,ssh_buffer buf, size_t maxlen);
int match_pattern_list(const char *string, const char *pattern,
size_t len, int dolower);
int match_hostname(const char *host, const char *pattern, unsigned int len);
#ifndef _WIN32
int match_cidr_address_list(const char *address,
const char *addrlist,
int sa_family);
#endif
/* connector.c */
int ssh_connector_set_event(ssh_connector connector, ssh_event event);

View File

@@ -39,6 +39,8 @@
# include <errno.h>
# include <signal.h>
# include <sys/wait.h>
# include <ifaddrs.h>
# include <net/if.h>
#endif
#include "libssh/config_parser.h"
@@ -160,7 +162,8 @@ enum ssh_config_match_e {
MATCH_HOST,
MATCH_ORIGINALHOST,
MATCH_USER,
MATCH_LOCALUSER
MATCH_LOCALUSER,
MATCH_LOCALNETWORK
};
struct ssh_config_match_keyword_table_s {
@@ -168,16 +171,18 @@ struct ssh_config_match_keyword_table_s {
enum ssh_config_match_e opcode;
};
static struct ssh_config_match_keyword_table_s ssh_config_match_keyword_table[] = {
{ "all", MATCH_ALL },
{ "canonical", MATCH_CANONICAL },
{ "final", MATCH_FINAL },
{ "exec", MATCH_EXEC },
{ "host", MATCH_HOST },
{ "originalhost", MATCH_ORIGINALHOST },
{ "user", MATCH_USER },
{ "localuser", MATCH_LOCALUSER },
{ NULL, MATCH_UNKNOWN },
static struct ssh_config_match_keyword_table_s
ssh_config_match_keyword_table[] = {
{"all", MATCH_ALL},
{"canonical", MATCH_CANONICAL},
{"final", MATCH_FINAL},
{"exec", MATCH_EXEC},
{"host", MATCH_HOST},
{"originalhost", MATCH_ORIGINALHOST},
{"user", MATCH_USER},
{"localuser", MATCH_LOCALUSER},
{"localnetwork", MATCH_LOCALNETWORK},
{NULL, MATCH_UNKNOWN},
};
static int ssh_config_parse_line(ssh_session session, const char *line,
@@ -572,6 +577,99 @@ ssh_config_make_absolute(ssh_session session,
return out;
}
#ifndef _WIN32
/**
* @brief Checks if host address matches the local network specified.
*
* Verify whether a local network interface address matches any of the CIDR
* patterns.
*
* @param addrlist The CIDR pattern-list to be checked, can contain both
* IPv4 and IPv6 addresses and has to be comma separated
* (',' only, space after comma not allowed).
*
* @param negate The negate condition. The return value is negated
* (returns 1 instead of 0 and vice versa).
*
* @return 1 if match found.
* @return 0 if no match found.
* @return -1 on errors.
*/
static int
ssh_match_localnetwork(const char *addrlist, bool negate)
{
struct ifaddrs *ifa = NULL, *ifaddrs = NULL;
int r, found = 0;
char address[NI_MAXHOST], err_msg[SSH_ERRNO_MSG_MAX] = {0};
socklen_t sa_len;
r = getifaddrs(&ifaddrs);
if (r != 0) {
SSH_LOG(SSH_LOG_WARN,
"Match localnetwork: getifaddrs() failed: %s",
ssh_strerror(r, err_msg, SSH_ERRNO_MSG_MAX));
return -1;
}
for (ifa = ifaddrs; ifa != NULL; ifa = ifa->ifa_next) {
if (ifa->ifa_addr == NULL || (ifa->ifa_flags & IFF_UP) == 0) {
continue;
}
switch (ifa->ifa_addr->sa_family) {
case AF_INET:
sa_len = sizeof(struct sockaddr_in);
break;
case AF_INET6:
sa_len = sizeof(struct sockaddr_in6);
break;
default:
SSH_LOG(SSH_LOG_TRACE,
"Interface %s: unsupported address family %d",
ifa->ifa_name,
ifa->ifa_addr->sa_family);
continue;
}
r = getnameinfo(ifa->ifa_addr,
sa_len,
address,
sizeof(address),
NULL,
0,
NI_NUMERICHOST);
if (r != 0) {
SSH_LOG(SSH_LOG_TRACE,
"Interface %s getnameinfo failed: %s",
ifa->ifa_name,
gai_strerror(r));
continue;
}
SSH_LOG(SSH_LOG_TRACE,
"Interface %s address %s",
ifa->ifa_name,
address);
r = match_cidr_address_list(address,
addrlist,
ifa->ifa_addr->sa_family);
if (r == 1) {
SSH_LOG(SSH_LOG_TRACE,
"Matched interface %s: address %s in %s",
ifa->ifa_name,
address,
addrlist);
found = 1;
break;
}
}
freeifaddrs(ifaddrs);
return (found == (negate ? 0 : 1));
}
#endif
static int
ssh_config_parse_line(ssh_session session,
const char *line,
@@ -795,6 +893,47 @@ ssh_config_parse_line(ssh_session session,
args++;
break;
#ifndef _WIN32
case MATCH_LOCALNETWORK:
/* Here we match only one argument */
p = ssh_config_get_str_tok(&s, NULL);
if (p == NULL || p[0] == '\0') {
ssh_set_error(session,
SSH_FATAL,
"line %d: ERROR - Match local network keyword"
"requires argument",
count);
SAFE_FREE(x);
return -1;
}
rv = match_cidr_address_list(NULL, p, -1);
if (rv == -1) {
ssh_set_error(session,
SSH_FATAL,
"line %d: ERROR - List invalid entry: %s",
count,
p);
SAFE_FREE(x);
return -1;
}
rv = ssh_match_localnetwork(p, negate);
if (rv == -1) {
ssh_set_error(session,
SSH_FATAL,
"line %d: ERROR - Error while retrieving "
"network interface information -"
" List entry: %s",
count,
p);
SAFE_FREE(x);
return -1;
}
result &= rv;
args++;
break;
#endif
case MATCH_UNKNOWN:
default:
ssh_set_error(session, SSH_FATAL,

View File

@@ -198,3 +198,378 @@ int match_pattern_list(const char *string, const char *pattern,
int match_hostname(const char *host, const char *pattern, unsigned int len) {
return match_pattern_list(host, pattern, len, 1);
}
#ifndef _WIN32
/**
* @brief Tries to match the host IPv6 address against a given network address
* with specified prefix length in CIDR notation.
*
* @param[in] host_addr The host address to verify.
*
* @param[in] net_addr The network id address against which the match is
* being verified
*
* @param[in] bits The prefix length
*
* @return 0 on a negative match.
* @return 1 on a positive match.
*/
static int
cidr_match_6(struct in6_addr *host_addr,
struct in6_addr *net_addr,
unsigned int bits)
{
const uint32_t *a = host_addr->s6_addr32;
const uint32_t *b = net_addr->s6_addr32;
unsigned int qwords_whole, bits_left;
/* The number of complete 32-bit words covered by the prefix */
qwords_whole = bits / 32;
/*
* The number of bits remaining in the incomplete (last) 32-bit word
* covered by the prefix
*/
bits_left = bits % 32;
if (qwords_whole) {
if (memcmp(a, b, qwords_whole * 4) != 0) {
return 0;
}
}
if (bits_left) {
if ((a[qwords_whole] ^ b[qwords_whole]) &
htonl((0xFFFFFFFFu << (32 - bits_left)) & 0xFFFFFFFFu)) {
return 0;
}
}
return 1;
}
/**
* @brief Tries to match the host IPv4 address against a given network address
* with specified prefix length in CIDR notation.
*
* @param[in] host_addr The host address to verify.
*
* @param[in] net_addr The network id address against which the match is
* being verified
*
* @param[in] bits The prefix length
*
* @return 0 on a negative match.
* @return 1 on a positive match.
*/
static int
cidr_match_4(struct in_addr *host_addr,
struct in_addr *net_addr,
unsigned int bits)
{
if (bits == 0) {
/* C99 6.5.7 (3): u32 << 32 is undefined behaviour */
return 1;
}
return !((host_addr->s_addr ^ net_addr->s_addr) &
htonl((0xFFFFFFFFu << (32 - bits)) & 0xFFFFFFFFu));
}
/**
* @brief Checks if the mask length is valid according to the address family
* (IPv4 or IPv6).
*
* @param[in] family The address family (e.g. AF_INET or AF_INET6)
*
* @param[in] mask The subnet mask (prefix)
*
* @return true if the mask length does not exceed the maximum valid length
* according to the address family (IPv4 or IPv6).
* @return false if the mask length exceeds the maximum valid length
* or there is no match with IPv4 or IPv6 address family.
*/
static bool
masklen_valid(int family, unsigned int mask)
{
switch (family) {
case AF_INET:
return mask <= 32;
case AF_INET6:
return mask <= 128;
default:
return false;
}
}
/**
* @brief Extracts address family given a network address.
*
* @param[in] address The network address.
*
* @return The value of the address family if no errors.
* @return -1 in case of errors.
*/
static int
get_address_family(const char *address)
{
struct addrinfo hints, *ai = NULL;
int rc = -1, rv;
ZERO_STRUCT(hints);
if (address == NULL) {
SSH_LOG(SSH_LOG_TRACE, "Bad arguments");
goto out;
}
hints.ai_flags = AI_NUMERICHOST;
rv = getaddrinfo(address, NULL, &hints, &ai);
if (rv != 0) {
SSH_LOG(SSH_LOG_TRACE,
"Couldn't get address information - getaddrinfo() failed: %s",
gai_strerror(rv));
goto out;
}
rc = ai->ai_family;
freeaddrinfo(ai);
out:
return rc;
}
/**
* @brief Tries to match the host address against a CIDR list provided
* by the user. If the host address family is unknown, it can be derived by
* passing -1 as sa_family argument.
*
* It can be also used to validate a CIDR list when the passed address is NULL
* and sa_family is -1.
*
* @param[in] address The host address to verify (NULL to validate CIDR list).
*
* @param[in] addrlist The CIDR list against which the match is being verified.
* The CIDR list can contain both IPv4 and IPv6 addresses
* and has to be comma separated
* (',' only, space after comma not allowed).
*
* @param[in] sa_family The socket address family (e.g. AF_INET or AF_INET6,
* -1 to validate CIDR list or unknown address family).
*
* @usage To validate CIDR list: match_cidr_address_list(NULL, addrlist, -1).
* @usage To verify a match with unknown address family:
* match_cidr_address_list(address, addrlist, -1).
* @return 1 only on positive match.
* @return 0 on negative match or valid CIDR list.
* @return -1 on errors or invalid CIDR list.
*/
int
match_cidr_address_list(const char *address,
const char *addrlist,
int sa_family)
{
char *list = NULL, *cp = NULL, *a = NULL, *b = NULL, *sp = NULL;
char addr_buffer[64], addr[NI_MAXHOST];
struct in_addr try_addr, match_addr;
struct in6_addr try_addr6, match_addr6;
unsigned long mask_len;
size_t addr_len, tmp_len;
int rc = 0, r, ai_family;
ZERO_STRUCT(try_addr);
ZERO_STRUCT(try_addr6);
ZERO_STRUCT(match_addr);
ZERO_STRUCT(match_addr6);
if (sa_family != AF_INET && sa_family != AF_INET6 && sa_family != -1) {
SSH_LOG(SSH_LOG_TRACE,
"Invalid argument: sa_family %d is not valid",
sa_family);
return -1;
}
if (address != NULL) {
strncpy(addr, address, NI_MAXHOST - 1);
/* Remove interface in case of IPv6 address: addr%interface */
a = strchr(addr, '%');
if (a != NULL) {
*a = '\0';
}
/*
* If sa_family is set to -1 and address is not NULL then
* the socket address family should be derived
*/
if (sa_family == -1) {
r = get_address_family(addr);
if (r == -1) {
SSH_LOG(SSH_LOG_TRACE,
"Failed to derive address family for address "
"\"%.100s\"",
addr);
return -1;
}
sa_family = r;
}
/*
* Translate host address from dot notation to binary network format
* according to family type,
* i.e. IPv4 (store in in_addr) or IPv6 (store in in6_addr)
*/
if (sa_family == AF_INET) {
if (inet_pton(AF_INET, addr, &try_addr) == 0) {
SSH_LOG(SSH_LOG_TRACE,
"Couldn't parse IPv4 address \"%.100s\"",
addr);
return -1;
}
} else if (sa_family == AF_INET6) {
if (inet_pton(AF_INET6, addr, &try_addr6) == 0) {
SSH_LOG(SSH_LOG_TRACE,
"Couldn't parse IPv6 address \"%.100s\"",
addr);
return -1;
}
} else {
SSH_LOG(SSH_LOG_TRACE,
"Address family %d for address \"%.100s\" "
"is not recognized",
sa_family,
addr);
return -1;
}
}
b = list = strdup(addrlist);
if (b == NULL) {
return -1;
}
while ((cp = strsep(&list, ",")) != NULL) {
if (*cp == '\0') {
SSH_LOG(SSH_LOG_TRACE, "Empty entry in list \"%.100s\"", b);
rc = -1;
break;
}
/*
* Stop junk from reaching address translation. +3 for the "/prefix".
* INET6_ADDRSTRLEN is 46 and includes space for '\0' terminator. The
* maximum IPv6 address printable is the one that carries IPv4 too.
* E.g. ffff:ffff:ffff:ffff:ffff:ffff:255.255.255.255 is 46 chars
* long ('\0' included) and the maximum prefix length possible is 96.
* This explains why +3. All the other IPv6 addresses with maximum /127
* prefix length (39 + 4) are covered just by INET6_ADDRSTRLEN itself
*/
addr_len = strlen(cp);
if (addr_len > INET6_ADDRSTRLEN + 3) {
SSH_LOG(SSH_LOG_TRACE,
"List entry \"%.100s\" too long: %zu > %d (MAX ALLOWED)",
cp,
addr_len,
INET6_ADDRSTRLEN + 3);
rc = -1;
break;
}
#define VALID_CIDR_CHARS "0123456789abcdefABCDEF.:/"
tmp_len = strspn(cp, VALID_CIDR_CHARS);
if (tmp_len != addr_len) {
SSH_LOG(SSH_LOG_TRACE,
"List entry \"%.100s\" contains invalid characters "
"-> \"%c\" is an invalid character",
cp,
cp[tmp_len]);
rc = -1;
break;
}
#undef VALID_CIDR_CHARS
strncpy(addr_buffer, cp, sizeof(addr_buffer) - 1);
sp = strchr(addr_buffer, '/');
if (sp != NULL) {
*sp = '\0';
sp++;
mask_len = strtoul(sp, &cp, 10);
if (*sp < '0' || *sp > '9' || *cp != '\0') {
SSH_LOG(SSH_LOG_TRACE, "Error while parsing prefix: %s", sp);
rc = -1;
break;
}
if (mask_len > 128) {
SSH_LOG(SSH_LOG_TRACE,
"Invalid prefix: %lu exceeds the maximum allowed "
"(>128)",
mask_len);
rc = -1;
break;
}
} else {
SSH_LOG(SSH_LOG_TRACE,
"Missing prefix length for list entry \"%.100s\"",
addr_buffer);
rc = -1;
break;
}
ai_family = get_address_family(addr_buffer);
if (ai_family == -1) {
SSH_LOG(SSH_LOG_TRACE,
"Couldn't get address family for \"%.100s\"",
addr_buffer);
rc = -1;
break;
}
if (ai_family == AF_INET) {
if (inet_pton(AF_INET, addr_buffer, &match_addr) == 0) {
SSH_LOG(SSH_LOG_TRACE,
"Couldn't parse IPv4 address \"%.100s\"",
addr_buffer);
rc = -1;
break;
}
} else if (ai_family == AF_INET6) {
if (inet_pton(AF_INET6, addr_buffer, &match_addr6) == 0) {
SSH_LOG(SSH_LOG_TRACE,
"Couldn't parse IPv6 address \"%.100s\"",
addr_buffer);
rc = -1;
break;
}
} else {
SSH_LOG(SSH_LOG_TRACE,
"Address family %d for address \"%.100s\" "
"is not recognized",
ai_family,
addr_buffer);
rc = -1;
break;
}
if (masklen_valid(ai_family, mask_len) != true) {
SSH_LOG(SSH_LOG_TRACE,
"Invalid mask length %lu for list entry \"%.100s\"",
mask_len,
addr_buffer);
rc = -1;
break;
}
/* Verify match between host address and network address*/
if (((ai_family == AF_INET && sa_family == AF_INET) &&
cidr_match_4(&try_addr, &match_addr, mask_len)) ||
((ai_family == AF_INET6 && sa_family == AF_INET6) &&
cidr_match_6(&try_addr6, &match_addr6, mask_len))) {
rc = 1;
break;
}
}
SAFE_FREE(b);
return rc;
}
#endif

View File

@@ -27,12 +27,12 @@
#ifndef _WIN32
/* This is needed for a standard getpwuid_r on opensolaris */
#define _POSIX_PTHREAD_SEMANTICS
#include <pwd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <netinet/in.h>
#include <pwd.h>
#include <sys/socket.h>
#include <sys/types.h>
#endif /* _WIN32 */
@@ -2226,5 +2226,4 @@ int ssh_check_username_syntax(const char *username)
return SSH_OK;
}
/** @} */