miniupnpd: Add option to match rules with regex

Some reports that a certain app is abusing UPnP for exploiting upload
bandwidth. This commit adds support to restrict UPnP rules to a regex.
By matching requester's description string against rule's regex, this
will make some obstacles for that app.
This commit is contained in:
yangfl 2022-10-16 10:16:11 +08:00 committed by Thomas Bernard
parent 59335e4637
commit 2ff8cb17da
8 changed files with 261 additions and 16 deletions

12
miniupnpd/configure vendored
View File

@ -15,7 +15,7 @@ UPNP_VERSION_MINOR=1
# input environment variables :
# IPV6, IGD2, STRICT, DEBUG, LEASFILE, VENDORCFG, PCP_PEER,
# PORTINUSE, DISABLEPPPCONN, FW, IPTABLESPATH, TARGET_OPENWRT,
# PORTINUSE, REGEX, DISABLEPPPCONN, FW, IPTABLESPATH, TARGET_OPENWRT,
# PKG_CONFIG, NO_BACKGROUND_NO_PIDFILE, DYNAMIC_OS_VERSION
# OS_NAME, OS_VERSION, OS_MACHINE, V6SOCKETS_ARE_V6ONLY
@ -33,6 +33,7 @@ case "$argv" in
--vendorcfg) VENDORCFG=1 ;;
--pcp-peer) PCP_PEER=1 ;;
--portinuse) PORTINUSE=1 ;;
--regex) REGEX=1 ;;
--uda-version=*)
UPNP_VERSION=$(echo $argv | cut -d= -f2)
UPNP_VERSION_MAJOR=$(echo $UPNP_VERSION | cut -s -d. -f1)
@ -67,6 +68,7 @@ case "$argv" in
echo " --vendorcfg enable configuration of manufacturer info"
echo " --pcp-peer enable PCP PEER operation"
echo " --portinuse enable port in use check"
echo " --regex enable description regex filter"
echo " --uda-version=x.x set advertised UPnP version (default to ${UPNP_VERSION_MAJOR}.${UPNP_VERSION_MINOR})"
echo " --disable-pppconn disable WANPPPConnection"
echo " --firewall=<name> force the firewall type (nftables, iptables, pf, ipf, ipfw)"
@ -753,6 +755,14 @@ else
fi
echo "" >> ${CONFIGFILE}
echo "/* Uncomment the following line to enable description regex filter */" >> ${CONFIGFILE}
if [ -n "$REGEX" ]; then
echo "#define ENABLE_REGEX" >> ${CONFIGFILE}
else
echo "/*#define ENABLE_REGEX*/" >> ${CONFIGFILE}
fi
echo "" >> ${CONFIGFILE}
echo "/* Define one or none of the two following macros in order to make some" >> ${CONFIGFILE}
echo " * clients happy. It will change the XML Root Description of the IGD." >> ${CONFIGFILE}
echo " * Enabling the Layer3Forwarding Service seems to be the more compatible" >> ${CONFIGFILE}

View File

@ -168,7 +168,7 @@ uuid=00000000-0000-0000-0000-000000000000
#force_igd_desc_v1=no
# UPnP permission rules
# (allow|deny) (external port range) IP/mask (internal port range)
# (allow|deny) (external port range) IP/mask (internal port range) (optional regex filter)
# A port range is <min port>-<max port> or <port> if there is only
# one port in the range.
# IP/mask format must be nnn.nnn.nnn.nnn/nn
@ -180,6 +180,8 @@ uuid=00000000-0000-0000-0000-000000000000
# also consider implementing network-specific restrictions
# CAUTION: failure to enforce any rules may permit insecure requests to be made!
allow 1024-65535 192.168.0.0/24 1024-65535
# disallow requests whose description string matches the given regex
# deny 1024-65535 192.168.1.0/24 1024-65535 "My evil app ver [[:digit:]]*"
allow 1024-65535 192.168.1.0/24 1024-65535
allow 1024-65535 192.168.0.0/23 22
allow 12345 192.168.7.113/32 54321

View File

@ -350,7 +350,7 @@ void ProcessIncomingNATPMPPacket(int s, unsigned char *msg_buff, int len,
}
break;
}
if(!check_upnp_rule_against_permissions(upnppermlist, num_upnpperm, eport, senderaddr->sin_addr, iport)) {
if(!check_upnp_rule_against_permissions(upnppermlist, num_upnpperm, eport, senderaddr->sin_addr, iport, "NAT-PMP")) {
eport++;
if(eport == 0) eport++; /* skip port zero */
continue;

View File

@ -320,6 +320,10 @@ freeoptions(void)
}
if(upnppermlist)
{
unsigned int i;
for (i = 0; i < num_upnpperm; i++) {
free_permission_line(upnppermlist + i);
}
free(upnppermlist);
upnppermlist = NULL;
num_upnpperm = 0;

View File

@ -934,7 +934,7 @@ static int CreatePCPMap_NAT(pcp_info_t *pcp_msg_info)
(!check_upnp_rule_against_permissions(upnppermlist,
num_upnpperm, pcp_msg_info->ext_port,
((struct in_addr*)pcp_msg_info->mapped_ip->s6_addr)[3],
pcp_msg_info->int_port)))) {
pcp_msg_info->int_port, pcp_msg_info->desc)))) {
if (pcp_msg_info->pfailure_present) {
return PCP_ERR_CANNOT_PROVIDE_EXTERNAL;
}

View File

@ -1,7 +1,7 @@
/* $Id: upnppermissions.c,v 1.20 2020/10/30 21:37:35 nanard Exp $ */
/* MiniUPnP project
* http://miniupnp.free.fr/ or https://miniupnp.tuxfamily.org/
* (c) 2006-2020 Thomas Bernard
* (c) 2006-2022 Thomas Bernard
* This software is subject to the conditions detailed
* in the LICENCE file provided within the distribution */
@ -13,12 +13,159 @@
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#ifdef ENABLE_REGEX
#include <regex.h>
#endif
#include "config.h"
#include "macros.h"
#include "upnppermissions.h"
static int
isodigit(char c)
{
return '0' <= c && c >= '7';
}
static char
hex2chr(char c)
{
if(c >= 'a')
return c - 'a';
if(c >= 'A')
return c - 'A';
return c - '0';
}
static char
unescape_char(const char * s, int * seqlen)
{
char c;
int len;
if(s[0] != '\\')
{
c = s[0];
len = 1;
}
else
{
s++;
c = s[0];
len = 2;
switch(s[0])
{
case 'a': c = '\a'; break;
case 'b': c = '\b'; break;
case 'f': c = '\f'; break;
case 'n': c = '\n'; break;
case 'r': c = '\r'; break;
case 't': c = '\t'; break;
case 'v': c = '\v'; break;
/* no need: escape the char itself
case '\\': c = '\\'; break;
case '\'': c = '\''; break;
case '"': c = '"'; break;
case '?': c = '?'; break;
*/
case 'x':
if(isxdigit(s[1]) && isxdigit(s[2]))
{
c = (hex2chr(s[1]) << 4) + hex2chr(s[2]);
len = 4;
}
break;
default:
if(isodigit(s[1]) && isodigit(s[2]) && isodigit(s[3]))
{
c = (hex2chr(s[0]) << 6) + (hex2chr(s[1]) << 3) + hex2chr(s[2]);
len = 4;
}
}
}
if(seqlen)
*seqlen = len;
return c;
}
/* get_next_token(s, &token, raw)
* put the unquoted/unescaped token in token and returns
* a pointer to the begining of the next token
* Do not unescape if raw is true */
static char *
get_next_token(const char * s, char ** token, int raw)
{
char deli;
const char * end;
/* skip any whitespace */
for(; isspace(*s); s++)
if(*s == '\0' || *s == '\n')
{
if(token)
*token = NULL;
return (char *) s;
}
/* find the start */
if(*s == '"' || *s == '\'')
{
deli = *s;
s++;
}
else
deli = 0;
/* find the end */
end = s;
for(; *end != '\0' && *end != '\n' && (deli ? *end != deli : !isspace(*end));
end++)
if(*end == '\\')
{
end++;
if(*end == '\0')
break;
}
/* save the token */
if(token)
{
unsigned int token_len;
unsigned int i;
token_len = end - s;
*token = strndup(s, token_len);
if(!*token)
return NULL;
for(i = 0; (*token)[i] != '\0'; i++)
{
int sequence_len;
if((*token)[i] != '\\')
continue;
if(raw && deli && (*token)[i + 1] != deli)
continue;
(*token)[i] = unescape_char(*token + i, &sequence_len);
memmove(*token + i + 1, *token + i + sequence_len,
token_len - i - sequence_len);
}
*token = realloc(*token, i);
}
/* return the beginning of the next token */
if(deli && *end == deli)
end++;
while(isspace(*end))
end++;
return (char *) end;
}
/* read_permission_line()
* parse the a permission line which format is :
* (deny|allow) [0-9]+(-[0-9]+) ip/mask [0-9]+(-[0-9]+)
* (deny|allow) [0-9]+(-[0-9]+) ip/mask [0-9]+(-[0-9]+) regex
* ip/mask is either 192.168.1.1/24 or 192.168.1.1/255.255.255.0
*/
int
@ -172,15 +319,72 @@ read_permission_line(struct upnpperm * perm,
{
return -1;
}
p = q;
/* fifth token: (optional) regex */
p = get_next_token(p, &perm->re, 1);
if(!p)
{
fprintf(stderr, "err when copying regex: out of memory\n");
return -1;
}
if(perm->re)
{
if(perm->re[0] == '\0')
{
free(perm->re);
perm->re = NULL;
}
else
{
#ifdef ENABLE_REGEX
/* icase: if case matters, it must be someone doing something nasty */
int err;
err = regcomp(&perm->regex, perm->re,
REG_EXTENDED | REG_ICASE | REG_NOSUB);
if(err)
{
char errbuf[256];
regerror(err, &perm->regex, errbuf, sizeof(errbuf));
fprintf(stderr, "err when compiling regex \"%s\": %s\n",
perm->re, errbuf);
free(perm->re);
perm->re = NULL;
return -1;
}
#else
fprintf(stderr, "MiniUPnP is not compiled with ENABLE_REGEX. "
"Please remove any regex filter and restart.\n");
free(perm->re);
perm->re = NULL;
return -1;
#endif
}
}
#ifdef DEBUG
printf("perm rule added : %s %hu-%hu %08x/%08x %hu-%hu\n",
printf("perm rule added : %s %hu-%hu %08x/%08x %hu-%hu %s\n",
(perm->type==UPNPPERM_ALLOW)?"allow":"deny",
perm->eport_min, perm->eport_max, ntohl(perm->address.s_addr),
ntohl(perm->mask.s_addr), perm->iport_min, perm->iport_max);
ntohl(perm->mask.s_addr), perm->iport_min, perm->iport_max,
(perm->re)?re:"");
#endif
return 0;
}
void
free_permission_line(struct upnpperm * perm)
{
if(perm->re)
{
free(perm->re);
perm->re = NULL;
#ifdef ENABLE_REGEX
regfree(&perm->regex);
#endif
}
}
#ifdef USE_MINIUPNPDCTL
void
write_permlist(int fd, const struct upnpperm * permary,
@ -194,14 +398,20 @@ write_permlist(int fd, const struct upnpperm * permary,
for(i = 0; i<nperms; i++)
{
perm = permary + i;
l = snprintf(buf, sizeof(buf), "%02d %s %hu-%hu %08x/%08x %hu-%hu\n",
l = snprintf(buf, sizeof(buf), "%02d %s %hu-%hu %08x/%08x %hu-%hu",
i,
(perm->type==UPNPPERM_ALLOW)?"allow":"deny",
(perm->type==UPNPPERM_ALLOW)?"allow":"deny",
perm->eport_min, perm->eport_max, ntohl(perm->address.s_addr),
ntohl(perm->mask.s_addr), perm->iport_min, perm->iport_max);
if(l<0)
return;
write(fd, buf, l);
if(perm->re)
{
write(fd, " ", 1);
write(fd, perm->re, strlen(perm->re));
}
write(fd, "\n", 1);
}
}
#endif
@ -211,7 +421,8 @@ write_permlist(int fd, const struct upnpperm * permary,
* 0 if no match */
static int
match_permission(const struct upnpperm * perm,
u_short eport, struct in_addr address, u_short iport)
u_short eport, struct in_addr address, u_short iport,
const char * desc)
{
if( (eport < perm->eport_min) || (perm->eport_max < eport))
return 0;
@ -220,6 +431,12 @@ match_permission(const struct upnpperm * perm,
if( (address.s_addr & perm->mask.s_addr)
!= (perm->address.s_addr & perm->mask.s_addr) )
return 0;
#ifdef ENABLE_REGEX
if(desc && perm->re && regexec(&perm->regex, desc, 0, NULL, 0) == REG_NOMATCH)
return 0;
#else
UNUSED(desc);
#endif
return 1;
}
@ -244,12 +461,12 @@ int
check_upnp_rule_against_permissions(const struct upnpperm * permary,
int n_perms,
u_short eport, struct in_addr address,
u_short iport)
u_short iport, const char * desc)
{
int i;
for(i=0; i<n_perms; i++)
{
if(match_permission(permary + i, eport, address, iport))
if(match_permission(permary + i, eport, address, iport, desc))
{
syslog(LOG_DEBUG,
"UPnP permission rule %d matched : port mapping %s",

View File

@ -11,6 +11,11 @@
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#ifdef ENABLE_REGEX
#include <regex.h>
#endif
#include "config.h"
/* UPnP permission rule samples:
@ -22,6 +27,10 @@ struct upnpperm {
u_short eport_min, eport_max; /* external port range */
struct in_addr address, mask; /* ip/mask */
u_short iport_min, iport_max; /* internal port range */
char * re;
#ifdef ENABLE_REGEX
regex_t regex; /* matching regex */
#endif
};
/* read_permission_line()
@ -36,6 +45,9 @@ int
read_permission_line(struct upnpperm * perm,
char * p);
void
free_permission_line(struct upnpperm * perm);
/* check_upnp_rule_against_permissions()
* returns: 0 if the upnp rule should be rejected,
* 1 if it could be accepted */
@ -43,7 +55,7 @@ int
check_upnp_rule_against_permissions(const struct upnpperm * permary,
int n_perms,
u_short eport, struct in_addr address,
u_short iport);
u_short iport, const char * desc);
/**
* Build an array of all allowed external ports (for the address and internal port)

View File

@ -351,9 +351,9 @@ upnp_redirect(const char * rhost, unsigned short eport,
}
if(!check_upnp_rule_against_permissions(upnppermlist, num_upnpperm,
eport, address, iport)) {
eport, address, iport, desc)) {
syslog(LOG_INFO, "redirection permission check failed for "
"%hu->%s:%hu %s", eport, iaddr, iport, protocol);
"%hu->%s:%hu %s %s", eport, iaddr, iport, protocol, desc);
return -3;
}