diff --git a/include/libssh/misc.h b/include/libssh/misc.h index fc8596f7..28982990 100644 --- a/include/libssh/misc.h +++ b/include/libssh/misc.h @@ -33,7 +33,7 @@ # endif /* _MSC_VER */ #else -# include +#include #endif /* _WIN32 */ #ifdef __cplusplus diff --git a/include/libssh/priv.h b/include/libssh/priv.h index 4434d143..42e55753 100644 --- a/include/libssh/priv.h +++ b/include/libssh/priv.h @@ -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); diff --git a/src/config.c b/src/config.c index 7135c3b1..79839007 100644 --- a/src/config.c +++ b/src/config.c @@ -39,6 +39,8 @@ # include # include # include +# include +# include #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, diff --git a/src/match.c b/src/match.c index 3e58f733..d04d66e0 100644 --- a/src/match.c +++ b/src/match.c @@ -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 diff --git a/src/misc.c b/src/misc.c index c7a9706c..463e85f2 100644 --- a/src/misc.c +++ b/src/misc.c @@ -27,12 +27,12 @@ #ifndef _WIN32 /* This is needed for a standard getpwuid_r on opensolaris */ #define _POSIX_PTHREAD_SEMANTICS -#include -#include -#include -#include #include #include +#include +#include +#include +#include #endif /* _WIN32 */ @@ -2226,5 +2226,4 @@ int ssh_check_username_syntax(const char *username) return SSH_OK; } - /** @} */