Compare commits

...

16 Commits

Author SHA1 Message Date
Simon Kelley
b1daf44954 Correct domain search algorithm.
For reasons unknown, I (srk) assumed that the orginal
substring domain matching algorithm was still in use,
where example.comKevin Darbyshire-Bryant <kevin@darbyshire-bryant.me.uk> would match eg. sexample.com

In fact the far more sensible label-based match, where
example.com (or .example.com) matches example.com and
www.example.com, but not sexample.com, has been in use
since release 2.22. This commit implements the 2.22 to 2.85
behaviour in the new domain-search code.

Thanks to Kevin Darbyshire-Bryant for spotting my mistake.
2021-06-24 23:28:47 +01:00
Simon Kelley
be291d979d Include EDNS0 in connmark REFUSED replies. 2021-06-21 16:59:42 +01:00
Simon Kelley
6d1edd8d32 Use correct packet-size limit in make_local_answer() 2021-06-21 15:59:07 +01:00
Simon Kelley
25ff956c7d Tidy up name buffer use in report_addresses().
Buffer may need to be twice MAXDNAME is escaping is
enabled in extract_name. The name may include weird characters.
2021-06-21 15:05:28 +01:00
Simon Kelley
38179500f8 CHANGELOG entry for new connmark code. 2021-06-21 14:35:36 +01:00
Simon Kelley
5f7be5f0d6 Fix compiler warning. 2021-06-21 14:31:54 +01:00
Etan Kissling
627056febb Connection track mark based DNS query filtering.
This extends query filtering support beyond what is currently possible
with the `--ipset` configuration option, by adding support for:
1) Specifying allowlists on a per-client basis, based on their
   associated Linux connection track mark.
2) Dynamic configuration of allowlists via Ubus.
3) Reporting when a DNS query resolves or is rejected via Ubus.
4) DNS name patterns containing wildcards.

Disallowed queries are not forwarded; they are rejected
with a REFUSED error code.

Signed-off-by: Etan Kissling <etan_kissling@apple.com>
(addressed reviewer feedback)
Signed-off-by: Etan Kissling <etan.kissling@gmail.com>
2021-06-21 14:14:55 +01:00
Simon Kelley
cbd76447fd Further work from a0a3b8ad3e
When query longer than longest domain, crop directly to length
of longest domain.
2021-06-21 00:01:51 +01:00
Simon Kelley
a60a233329 Fix bug introduced in 6860cf932b
Breakage 0f --no-rebind-domain due to incomplete edit.

Thanks to Kevin Darbyshire-Bryant for spotting this.
2021-06-20 23:02:54 +01:00
Simon Kelley
a0a3b8ad3e Fix bug in 6860cf932b
The optimisation based on the assumption that
"try now points to the last domain that sorts before the query"
fails in the specific edge case that the query sorts before
_any_ of the server domains. Handle this case.

Thanks to Xingcong Li for finding a test case for this bug.
2021-06-20 22:57:54 +01:00
Simon Kelley
d0ae3f5a4d Fix specific NOERR/NXDOMAIN confusion.
In the specific case of configuring an A record for a domain

address=/example.com/1.2.3.4

queries for *example.com for any other type will now return
NOERR, and not the previous erroneous NXDOMAIN. The same thing
applies for

address=/example.com/::1:2:3:4
address=/example.com/#
2021-06-17 23:11:17 +01:00
Simon Kelley
6860cf932b Optimise lokkup_domain() 2021-06-17 21:30:40 +01:00
Simon Kelley
0276e0805b merge development machines.
Merge branch 'master' of ssh://thekelleys.org.uk/var/local/git/dnsmasq
2021-06-16 14:05:49 +01:00
Simon Kelley
06ff3d8a26 Log the correct name when we retry a DNSSEC query.
If we retry a DNSSEC query because our client retries on us, and
we have an answer but are waiting on a DNSSEC query to validate it,
log the name of the DNSSEC query, not the client's query.
2021-06-16 13:59:57 +01:00
Simon Kelley
1a3b69aa56 Fix error in new domain-search code.
SERV_USE_RESOLV set implies struct serv_local,
so don't can't set ->arrayposn

Thanks to Xingcong Li for the cod review which led to this.
2021-06-16 09:57:41 +01:00
Simon Kelley
8237d06ab7 Typo in FAQ.
Reported by Alexander Traud.
2021-06-15 23:14:59 +01:00
13 changed files with 1169 additions and 119 deletions

View File

@@ -64,6 +64,18 @@ version 2.86
queries. The requesting address and port have been removed from
DNSSEC logging lines, since this is no longer strictly defined.
Connection track mark based DNS query filtering. Thanks to
Etan Kissling for implementing this It extends query filtering
support beyond what is currently possible
with the `--ipset` configuration option, by adding support for:
1) Specifying allowlists on a per-client basis, based on their
associated Linux connection track mark.
2) Dynamic configuration of allowlists via Ubus.
3) Reporting when a DNS query resolves or is rejected via Ubus.
4) DNS name patterns containing wildcards.
Disallowed queries are not forwarded; they are rejected
with a REFUSED error code.
version 2.85
Fix problem with DNS retries in 2.83/2.84.

2
FAQ
View File

@@ -236,7 +236,7 @@ Q: What network types are supported by the DHCP server?
A: Ethernet (and 802.11 wireless) are supported on all platforms. On
Linux all network types (including FireWire) are supported.
Q: What are these strange "bind-interface" and "bind-dynamic" options?
Q: What are these strange "bind-interfaces" and "bind-dynamic" options?
A: Dnsmasq from v2.63 can operate in one of three different "networking
modes". This is unfortunate as it requires users configuring dnsmasq

View File

@@ -79,7 +79,7 @@ copts_conf = .copts_$(sum)
objs = cache.o rfc1035.o util.o option.o forward.o network.o \
dnsmasq.o dhcp.o lease.o rfc2131.o netlink.o dbus.o bpf.o \
helper.o tftp.o log.o conntrack.o dhcp6.o rfc3315.o \
dhcp-common.o outpacket.o radv.o slaac.o auth.o ipset.o \
dhcp-common.o outpacket.o radv.o slaac.o auth.o ipset.o pattern.o \
domain.o dnssec.o blockdata.o tables.o loop.o inotify.o \
poll.o rrfilter.o edns0.o arp.o crypto.o dump.o ubus.o \
metrics.o hash-questions.o domain-match.o

View File

@@ -371,7 +371,10 @@ provides service at that name, rather than the default which is
.TP
.B --enable-ubus[=<service-name>]
Enable dnsmasq UBus interface. It sends notifications via UBus on
DHCPACK and DHCPRELEASE events. Furthermore it offers metrics.
DHCPACK and DHCPRELEASE events. Furthermore it offers metrics
and allows configuration of Linux connection track mark based filtering.
When DNS query filtering based on Linux connection track marks is enabled
UBus notifications are generated for each resolved or filtered DNS query.
Requires that dnsmasq has been built with UBus support. If the service
name is given, dnsmasq provides service at that namespace, rather than
the default which is
@@ -536,6 +539,32 @@ These IP sets must already exist. See
.BR ipset (8)
for more details.
.TP
.B --connmark-allowlist-enable[=<mask>]
Enables filtering of incoming DNS queries with associated Linux connection track marks
according to individual allowlists configured via a series of \fB--connmark-allowlist\fP
options. Disallowed queries are not forwarded; they are rejected with a REFUSED error code.
DNS queries are only allowed if they do not have an associated Linux connection
track mark, or if the queried domains match the configured DNS patterns for the
associated Linux connection track mark. If no allowlist is configured for a
Linux connection track mark, all DNS queries associated with that mark are rejected.
If a mask is specified, Linux connection track marks are first bitwise ANDed
with the given mask before being processed.
.TP
.B --connmark-allowlist=<connmark>[/<mask>][,<pattern>[/<pattern>...]]
Configures the DNS patterns that are allowed in DNS queries associated with
the given Linux connection track mark.
If a mask is specified, Linux connection track marks are first bitwise ANDed
with the given mask before they are compared to the given connection track mark.
Patterns follow the syntax of DNS names, but additionally allow the wildcard
character "*" to be used up to twice per label to match 0 or more characters
within that label. Note that the wildcard never matches a dot (e.g., "*.example.com"
matches "api.example.com" but not "api.us.example.com"). Patterns must be
fully qualified, i.e., consist of at least two labels. The final label must not be
fully numeric, and must not be the "local" pseudo-TLD. A pattern must end with at least
two literal (non-wildcard) labels.
Instead of a pattern, "*" can be specified to disable allowlist filtering
for a given Linux connection track mark entirely.
.TP
.B \-m, --mx-host=<mx name>[[,<hostname>],<preference>]
Return an MX record named <mx name> pointing to the given hostname (if
given), or

View File

@@ -135,6 +135,13 @@ int main (int argc, char **argv)
}
#endif
#if defined(HAVE_CONNTRACK) && defined(HAVE_UBUS)
/* CONNTRACK UBUS code uses this buffer, so if not allocated above,
we need to allocate it here. */
if (option_bool(OPT_CMARK_ALST_EN) && !daemon->workspacename)
daemon->workspacename = safe_malloc(MAXDNAME);
#endif
#ifdef HAVE_DHCP
if (!daemon->lease_file)
{

View File

@@ -272,7 +272,8 @@ struct event_desc {
#define OPT_LOG_DEBUG 62
#define OPT_UMBRELLA 63
#define OPT_UMBRELLA_DEVID 64
#define OPT_LAST 65
#define OPT_CMARK_ALST_EN 65
#define OPT_LAST 66
#define OPTION_BITS (sizeof(unsigned int)*8)
#define OPTION_SIZE ( (OPT_LAST/OPTION_BITS)+((OPT_LAST%OPTION_BITS)!=0) )
@@ -564,7 +565,7 @@ struct randfd_list {
struct server {
int flags;
u16 flags, domain_len;
char *domain;
struct server *next;
int serial, arrayposn;
@@ -583,23 +584,23 @@ struct server {
#endif
};
/* First three fields must match struct server in next three definitions.. */
/* First four fields must match struct server in next three definitions.. */
struct serv_addr4 {
int flags;
u16 flags, domain_len;
char *domain;
struct server *next;
struct in_addr addr;
};
struct serv_addr6 {
int flags;
u16 flags, domain_len;
char *domain;
struct server *next;
struct in6_addr addr;
};
struct serv_local {
int flags;
u16 flags, domain_len;
char *domain;
struct server *next;
};
@@ -610,6 +611,12 @@ struct ipsets {
struct ipsets *next;
};
struct allowlist {
u32 mark, mask;
char **patterns;
struct allowlist *next;
};
struct irec {
union mysockaddr addr;
struct in_addr netmask; /* only valid for IPv4 */
@@ -1086,6 +1093,8 @@ extern struct daemon {
struct server *servers, *local_domains, **serverarray, *no_rebind;
int serverarraysz;
struct ipsets *ipsets;
u32 allowlist_mask;
struct allowlist *allowlists;
int log_fac; /* log facility */
char *log_file; /* optional log file */
int max_logs; /* queue limit */
@@ -1275,6 +1284,9 @@ void setup_reply(struct dns_header *header, unsigned int flags);
int extract_addresses(struct dns_header *header, size_t qlen, char *name,
time_t now, char **ipsets, int is_sign, int check_rebind,
int no_cache_dnssec, int secure, int *doctored);
#if defined(HAVE_CONNTRACK) && defined(HAVE_UBUS)
void report_addresses(struct dns_header *header, size_t len, u32 mark);
#endif
size_t answer_request(struct dns_header *header, char *limit, size_t qlen,
struct in_addr local_addr, struct in_addr local_netmask,
time_t now, int ad_reqd, int do_bit, int have_pseudoheader);
@@ -1381,7 +1393,7 @@ void set_option_bool(unsigned int opt);
void reset_option_bool(unsigned int opt);
struct hostsfile *expand_filelist(struct hostsfile *list);
char *parse_server(char *arg, union mysockaddr *addr,
union mysockaddr *source_addr, char *interface, int *flags);
union mysockaddr *source_addr, char *interface, u16 *flags);
int option_read_dynfile(char *file, int flags);
/* forward.c */
@@ -1546,6 +1558,10 @@ char *ubus_init(void);
void set_ubus_listeners(void);
void check_ubus_listeners(void);
void ubus_event_bcast(const char *type, const char *mac, const char *ip, const char *name, const char *interface);
# ifdef HAVE_CONNTRACK
void ubus_event_bcast_connmark_allowlist_refused(u32 mark, const char *name);
void ubus_event_bcast_connmark_allowlist_resolved(u32 mark, const char *pattern, const char *ip, u32 ttl);
# endif
#endif
/* ipset.c */
@@ -1554,6 +1570,13 @@ void ipset_init(void);
int add_to_ipset(const char *setname, const union all_addr *ipaddr, int flags, int remove);
#endif
/* pattern.c */
#ifdef HAVE_CONNTRACK
int is_valid_dns_name(const char *value);
int is_valid_dns_name_pattern(const char *value);
int is_dns_name_matching_pattern(const char *name, const char *pattern);
#endif
/* helper.c */
#if defined(HAVE_SCRIPT)
int create_helper(int event_fd, int err_fd, uid_t uid, gid_t gid, long max_fd);
@@ -1726,7 +1749,7 @@ int lookup_domain(char *qdomain, int flags, int *lowout, int *highout);
int filter_servers(int seed, int flags, int *lowout, int *highout);
int is_local_answer(time_t now, int first, char *name);
size_t make_local_answer(int flags, int gotname, size_t size, struct dns_header *header,
char *name, int first, int last);
char *name, char *limit, int first, int last);
int server_samegroup(struct server *a, struct server *b);
#ifdef HAVE_DNSSEC
int dnssec_server(struct server *server, char *keyname, int *firstp, int *lastp);

View File

@@ -16,7 +16,7 @@
#include "dnsmasq.h"
static int order(char *qdomain, int leading_dot, size_t qlen, struct server *serv);
static int order(char *qdomain, size_t qlen, struct server *serv);
static int order_qsort(const void *a, const void *b);
static int order_servers(struct server *s, struct server *s2);
@@ -62,7 +62,7 @@ void build_server_array(void)
/* servers need the location in the array to find all the whole
set of equivalent servers from a pointer to a single one. */
for (count = 0; count < daemon->serverarraysz; count++)
if (!(daemon->serverarray[count]->flags & SERV_LITERAL_ADDRESS))
if (!(daemon->serverarray[count]->flags & (SERV_LITERAL_ADDRESS | SERV_USE_RESOLV)))
daemon->serverarray[count]->arrayposn = count;
}
@@ -75,12 +75,14 @@ void build_server_array(void)
A flag of F_DNSSECOK returns a DNSSEC capable server only and
also disables NODOTS servers from consideration.
A flag of F_DOMAINSRV returns a domain-specific server only.
A flag of F_CONFIG returns anything that generates a local
reply of IPv4 or IPV6.
return 0 if nothing found, 1 otherwise.
*/
int lookup_domain(char *qdomain, int flags, int *lowout, int *highout)
{
int rc, nodots, leading_dot = 1;
ssize_t qlen, maxlen;
int rc, crop_query, nodots;
ssize_t qlen;
int try, high, low = 0;
int nlow = 0, nhigh = 0;
char *cp;
@@ -89,8 +91,6 @@ int lookup_domain(char *qdomain, int flags, int *lowout, int *highout)
if (daemon->serverarraysz == 0)
return 0;
maxlen = strlen(daemon->serverarray[0]->domain);
/* find query length and presence of '.' */
for (cp = qdomain, nodots = 1, qlen = 0; *cp; qlen++, cp++)
if (*cp == '.')
@@ -101,56 +101,74 @@ int lookup_domain(char *qdomain, int flags, int *lowout, int *highout)
if (qlen == 0 || flags & F_DNSSECOK)
nodots = 0;
/* No point trying to match more than the largest server domain */
if (qlen > maxlen)
{
qdomain += qlen - maxlen;
qlen = maxlen;
leading_dot = 0;
}
/* Search shorter and shorter RHS substrings for a match */
while (qlen >= 0)
{
/* Note that when we chop off a character, all the possible matches
/* Note that when we chop off a label, all the possible matches
MUST be at a larger index than the nearest failing match with one more
character, since the array is sorted longest to smallest. Hence
we don't reset low here. */
we don't reset low to zero here, we can go further below and crop the
search string to the size of the largest remaining server
when this match fails. */
high = daemon->serverarraysz;
crop_query = 1;
/* binary search */
do
while (1)
{
try = (low + high)/2;
if ((rc = order(qdomain, leading_dot, qlen, daemon->serverarray[try])) == 0)
if ((rc = order(qdomain, qlen, daemon->serverarray[try])) == 0)
break;
if (rc < 0)
if (rc < 0)
{
if (high == try)
break;
{
/* qdomain is longer or same length as longest domain, and try == 0
crop the query to the longest domain. */
crop_query = qlen - daemon->serverarray[try]->domain_len;
break;
}
high = try;
}
else
{
if (low == try)
break;
{
/* try now points to the last domain that sorts before the query, so
we know that a substring of the query shorter than it is required to match, so
find the largest domain that's shorter than try. Note that just going to
try+1 is not optimal, consider searching bbb in (aaa,ccc,bb). try will point
to aaa, since ccc sorts after bbb, but the first domain that has a chance to
match is bb. So find the length of the first domain later than try which is
is shorter than it.
There's a nasty edge case when qdomain sorts before _any_ of the
server domains, where try _doesn't point_ to the last domain that sorts
before the query, since no such domain exists. In that case, the loop
exits via the rc < 0 && high == try path above and this code is
not executed. */
ssize_t len, old = daemon->serverarray[try]->domain_len;
while (++try != daemon->serverarraysz)
{
if (old != (len = daemon->serverarray[try]->domain_len))
{
crop_query = qlen - len;
break;
}
}
break;
}
low = try;
}
}
while (low != high);
};
if (rc == 0)
{
/* We've matched a setting which says to use servers without a domain.
Continue the search with empty query (the last character gets stripped
by the loop. */
Continue the search with empty query */
if (daemon->serverarray[try]->flags & SERV_USE_RESOLV)
{
qdomain += qlen - 1;
qlen = 1;
}
crop_query = qlen;
else
{
/* We have a match, but it may only be (say) an IPv6 address, and
@@ -160,27 +178,30 @@ int lookup_domain(char *qdomain, int flags, int *lowout, int *highout)
break;
}
}
/* crop_query must be at least one always. */
if (crop_query == 0)
crop_query = 1;
if (leading_dot)
leading_dot = 0;
else
{
qlen--;
qdomain++;
}
/* strip chars off the query based on the largest possible remaining match,
then continue to the start of the next label. */
qlen -= crop_query;
qdomain += crop_query;
while (qlen > 0 && (*(qdomain-1) != '.'))
qlen--, qdomain++;
}
/* domain has no dots, and we have at least one server configured to handle such,
These servers always sort to the very end of the array.
A configured server eg server=/lan/ will take precdence. */
if (nodots &&
(daemon->serverarray[daemon->serverarraysz-1]->flags & SERV_FOR_NODOTS) &&
(nlow == nhigh || strlen(daemon->serverarray[nlow]->domain) == 0))
(nlow == nhigh || daemon->serverarray[nlow]->domain_len == 0))
filter_servers(daemon->serverarraysz-1, flags, &nlow, &nhigh);
/* F_DOMAINSRV returns only domain-specific servers, so if we got to a
general server, return empty set. */
if (nlow != nhigh && (flags & F_DOMAINSRV) && strlen(daemon->serverarray[nlow]->domain) == 0)
if (nlow != nhigh && (flags & F_DOMAINSRV) && daemon->serverarray[nlow]->domain_len == 0)
nlow = nhigh;
if (lowout)
@@ -222,47 +243,56 @@ int filter_servers(int seed, int flags, int *lowout, int *highout)
See which of those match our query in that priority order and narrow (low, high) */
for (i = nlow; i < nhigh && (daemon->serverarray[i]->flags & SERV_6ADDR); i++);
if (i != nlow && (flags & F_IPV6))
#define SERV_LOCAL_ADDRESS (SERV_6ADDR | SERV_4ADDR | SERV_ALL_ZEROS)
for (i = nlow; (flags & F_CONFIG) && i < nhigh && (daemon->serverarray[i]->flags & SERV_LOCAL_ADDRESS); i++);
if (i != nlow)
nhigh = i;
else
{
nlow = i;
for (i = nlow; i < nhigh && (daemon->serverarray[i]->flags & SERV_4ADDR); i++);
for (i = nlow; i < nhigh && (daemon->serverarray[i]->flags & SERV_6ADDR); i++);
if (i != nlow && (flags & F_IPV4))
if (i != nlow && (flags & F_IPV6))
nhigh = i;
else
{
nlow = i;
for (i = nlow; i < nhigh && (daemon->serverarray[i]->flags & SERV_ALL_ZEROS); i++);
for (i = nlow; i < nhigh && (daemon->serverarray[i]->flags & SERV_4ADDR); i++);
if (i != nlow && (flags & (F_IPV4 | F_IPV6)))
if (i != nlow && (flags & F_IPV4))
nhigh = i;
else
{
nlow = i;
for (i = nlow; i < nhigh && (daemon->serverarray[i]->flags & SERV_LITERAL_ADDRESS); i++);
for (i = nlow; i < nhigh && (daemon->serverarray[i]->flags & SERV_ALL_ZEROS); i++);
/* --local=/domain/, only return if we don't need a server. */
if (i != nlow && !(flags & (F_DNSSECOK | F_DOMAINSRV | F_SERVER)))
if (i != nlow && (flags & (F_IPV4 | F_IPV6)))
nhigh = i;
else
{
nlow = i;
/* If we want a server that can do DNSSEC, and this one can't,
return nothing. */
if ((flags & F_DNSSECOK) && !(daemon->serverarray[nlow]->flags & SERV_DO_DNSSEC))
nlow = nhigh;
for (i = nlow; i < nhigh && (daemon->serverarray[i]->flags & SERV_LITERAL_ADDRESS); i++);
/* --local=/domain/, only return if we don't need a server. */
if (i != nlow && !(flags & (F_DNSSECOK | F_DOMAINSRV | F_SERVER)))
nhigh = i;
else
{
nlow = i;
/* If we want a server that can do DNSSEC, and this one can't,
return nothing. */
if ((flags & F_DNSSECOK) && !(daemon->serverarray[nlow]->flags & SERV_DO_DNSSEC))
nlow = nhigh;
}
}
}
}
}
*lowout = nlow;
*highout = nhigh;
@@ -273,7 +303,7 @@ int is_local_answer(time_t now, int first, char *name)
{
int flags = 0;
int rc = 0;
if ((flags = daemon->serverarray[first]->flags) & SERV_LITERAL_ADDRESS)
{
if (flags & SERV_4ADDR)
@@ -283,13 +313,25 @@ int is_local_answer(time_t now, int first, char *name)
else if (flags & SERV_ALL_ZEROS)
rc = F_IPV4 | F_IPV6;
else
rc = check_for_local_domain(name, now) ? F_NOERR : F_NXDOMAIN;
{
/* argument first is the first struct server which matches the query type;
now roll back to the server which is just the same domain, to check if that
provides an answer of a different type. */
for (;first > 0 && order_servers(daemon->serverarray[first-1], daemon->serverarray[first]) == 0; first--);
if ((daemon->serverarray[first]->flags & SERV_LOCAL_ADDRESS) ||
check_for_local_domain(name, now))
rc = F_NOERR;
else
rc = F_NXDOMAIN;
}
}
return rc;
}
size_t make_local_answer(int flags, int gotname, size_t size, struct dns_header *header, char *name, int first, int last)
size_t make_local_answer(int flags, int gotname, size_t size, struct dns_header *header, char *name, char *limit, int first, int last)
{
int trunc = 0;
unsigned char *p;
@@ -315,7 +357,7 @@ size_t make_local_answer(int flags, int gotname, size_t size, struct dns_header
addr.addr4 = srv->addr;
header->ancount = htons(ntohs(header->ancount) + 1);
add_resource_record(header, ((char *)header) + 65536, &trunc, sizeof(struct dns_header), &p, daemon->local_ttl, NULL, T_A, C_IN, "4", &addr);
add_resource_record(header, limit, &trunc, sizeof(struct dns_header), &p, daemon->local_ttl, NULL, T_A, C_IN, "4", &addr);
log_query((flags | F_CONFIG | F_FORWARD) & ~F_IPV6, name, (union all_addr *)&addr, NULL);
}
@@ -330,7 +372,7 @@ size_t make_local_answer(int flags, int gotname, size_t size, struct dns_header
addr.addr6 = srv->addr;
header->ancount = htons(ntohs(header->ancount) + 1);
add_resource_record(header, ((char *)header) + 65536, &trunc, sizeof(struct dns_header), &p, daemon->local_ttl, NULL, T_AAAA, C_IN, "6", &addr);
add_resource_record(header, limit, &trunc, sizeof(struct dns_header), &p, daemon->local_ttl, NULL, T_AAAA, C_IN, "6", &addr);
log_query((flags | F_CONFIG | F_FORWARD) & ~F_IPV4, name, (union all_addr *)&addr, NULL);
}
@@ -372,20 +414,16 @@ int dnssec_server(struct server *server, char *keyname, int *firstp, int *lastp)
#endif
/* order by size, then by dictionary order */
static int order(char *qdomain, int leading_dot, size_t qlen, struct server *serv)
static int order(char *qdomain, size_t qlen, struct server *serv)
{
size_t dlen = 0;
int rc;
/* servers for dotless names always sort last
searched for name is never dotless. */
if (serv->flags & SERV_FOR_NODOTS)
return -1;
if (leading_dot)
qlen++;
dlen = strlen(serv->domain);
dlen = serv->domain_len;
if (qlen < dlen)
return 1;
@@ -393,22 +431,18 @@ static int order(char *qdomain, int leading_dot, size_t qlen, struct server *ser
if (qlen > dlen)
return -1;
if (leading_dot && (rc = '.' - serv->domain[0]) != 0)
return rc;
return strcmp(qdomain, leading_dot ? &serv->domain[1] : serv->domain);
return strcmp(qdomain, serv->domain);
}
static int order_servers(struct server *s1, struct server *s2)
{
size_t dlen = strlen(s1->domain);
/* need full comparison of dotless servers in
order_qsort() and filter_servers() */
/* need full comparison of dotless servers in
order_qsort() and filter_servers() */
if (s1->flags & SERV_FOR_NODOTS)
if (s1->flags & SERV_FOR_NODOTS)
return (s2->flags & SERV_FOR_NODOTS) ? 0 : 1;
return order(s1->domain, 0, dlen, s2);
return order(s1->domain, s1->domain_len, s2);
}
static int order_qsort(const void *a, const void *b)

View File

@@ -153,9 +153,8 @@ static int domain_no_rebind(char *domain)
struct server *serv;
int dlen = (int)strlen(domain);
/* flags is misused to hold length of domain. */
for (serv = daemon->no_rebind; serv; serv = serv->next)
if (dlen >= serv->flags && strcmp(serv->domain, &domain[dlen - serv->flags]) == 0)
if (dlen >= serv->domain_len && strcmp(serv->domain, &domain[dlen - serv->domain_len]) == 0)
return 1;
return 0;
@@ -340,7 +339,9 @@ static int forward_query(int udpfd, union mysockaddr *udpaddr,
blockdata_retrieve(forward->stash, forward->stash_len, (void *)header);
plen = forward->stash_len;
/* get query for logging. */
extract_request(header, plen, daemon->namebuff, NULL);
if (find_pseudoheader(header, plen, NULL, &pheader, &is_sign, NULL) && !is_sign)
PUTSHORT(SAFE_PKTSZ, pheader);
@@ -524,12 +525,22 @@ static int forward_query(int udpfd, union mysockaddr *udpaddr,
reply:
if (udpfd != -1)
{
if (!(plen = make_local_answer(flags, gotname, plen, header, daemon->namebuff, first, last)))
if (!(plen = make_local_answer(flags, gotname, plen, header, daemon->namebuff, limit, first, last)))
return 0;
if (oph)
plen = add_pseudoheader(header, plen, (unsigned char *)limit, daemon->edns_pktsz, 0, NULL, 0, do_bit, 0);
#if defined(HAVE_CONNTRACK) && defined(HAVE_UBUS)
if (option_bool(OPT_CMARK_ALST_EN))
{
unsigned int mark;
int have_mark = get_incoming_mark(udpaddr, dst_addr, /* istcp: */ 0, &mark);
if (have_mark && ((u32)mark & daemon->allowlist_mask))
report_addresses(header, plen, mark);
}
#endif
send_from(udpfd, option_bool(OPT_NOWILD) || option_bool(OPT_CLEVERBIND), (char *)header, plen, udpaddr, dst_addr, dst_iface);
}
@@ -662,16 +673,19 @@ static size_t process_reply(struct dns_header *header, time_t now, struct server
int doctored = 0;
if (rcode == NXDOMAIN &&
extract_request(header, n, daemon->namebuff, NULL) &&
check_for_local_domain(daemon->namebuff, now))
extract_request(header, n, daemon->namebuff, NULL))
{
/* if we forwarded a query for a locally known name (because it was for
an unknown type) and the answer is NXDOMAIN, convert that to NODATA,
since we know that the domain exists, even if upstream doesn't */
munged = 1;
header->hb3 |= HB3_AA;
SET_RCODE(header, NOERROR);
cache_secure = 0;
if (check_for_local_domain(daemon->namebuff, now) ||
lookup_domain(daemon->namebuff, F_CONFIG, NULL, NULL))
{
/* if we forwarded a query for a locally known name (because it was for
an unknown type) and the answer is NXDOMAIN, convert that to NODATA,
since we know that the domain exists, even if upstream doesn't */
munged = 1;
header->hb3 |= HB3_AA;
SET_RCODE(header, NOERROR);
cache_secure = 0;
}
}
if (extract_addresses(header, n, daemon->namebuff, now, sets, is_sign, check_rebind, no_cache, cache_secure, &doctored))
@@ -1148,6 +1162,16 @@ static void return_reply(time_t now, struct frec *forward, struct dns_header *he
dump_packet(DUMP_REPLY, daemon->packet, (size_t)nn, NULL, &src->source);
#endif
#if defined(HAVE_CONNTRACK) && defined(HAVE_UBUS)
if (option_bool(OPT_CMARK_ALST_EN))
{
unsigned int mark;
int have_mark = get_incoming_mark(&src->source, &src->dest, /* istcp: */ 0, &mark);
if (have_mark && ((u32)mark & daemon->allowlist_mask))
report_addresses(header, nn, mark);
}
#endif
send_from(src->fd, option_bool(OPT_NOWILD) || option_bool (OPT_CLEVERBIND), daemon->packet, nn,
&src->source, &src->dest, src->iface);
@@ -1164,6 +1188,47 @@ static void return_reply(time_t now, struct frec *forward, struct dns_header *he
}
#ifdef HAVE_CONNTRACK
static int is_query_allowed_for_mark(u32 mark, const char *name)
{
int is_allowable_name, did_validate_name = 0;
struct allowlist *allowlists;
char **patterns_pos;
for (allowlists = daemon->allowlists; allowlists; allowlists = allowlists->next)
if (allowlists->mark == (mark & daemon->allowlist_mask & allowlists->mask))
for (patterns_pos = allowlists->patterns; *patterns_pos; patterns_pos++)
{
if (!strcmp(*patterns_pos, "*"))
return 1;
if (!did_validate_name)
{
is_allowable_name = name ? is_valid_dns_name(name) : 0;
did_validate_name = 1;
}
if (is_allowable_name && is_dns_name_matching_pattern(name, *patterns_pos))
return 1;
}
return 0;
}
static size_t answer_disallowed(struct dns_header *header, size_t qlen, u32 mark, const char *name)
{
unsigned char *p;
#ifdef HAVE_UBUS
if (name)
ubus_event_bcast_connmark_allowlist_refused(mark, name);
#endif
setup_reply(header, /* flags: */ 0);
if (!(p = skip_questions(header, qlen)))
return 0;
return p - (unsigned char *)header;
}
#endif
void receive_query(struct listener *listen, time_t now)
{
struct dns_header *header = (struct dns_header *)daemon->packet;
@@ -1175,6 +1240,11 @@ void receive_query(struct listener *listen, time_t now)
size_t m;
ssize_t n;
int if_index = 0, auth_dns = 0, do_bit = 0, have_pseudoheader = 0;
#ifdef HAVE_CONNTRACK
unsigned int mark = 0;
int have_mark = 0;
int is_single_query = 0, allowed = 1;
#endif
#ifdef HAVE_AUTH
int local_auth = 0;
#endif
@@ -1403,6 +1473,11 @@ void receive_query(struct listener *listen, time_t now)
#ifdef HAVE_DUMPFILE
dump_packet(DUMP_QUERY, daemon->packet, (size_t)n, &source_addr, NULL);
#endif
#ifdef HAVE_CONNTRACK
if (option_bool(OPT_CMARK_ALST_EN))
have_mark = get_incoming_mark(&source_addr, &dst_addr, /* istcp: */ 0, &mark);
#endif
if (extract_request(header, (size_t)n, daemon->namebuff, &type))
{
@@ -1413,6 +1488,10 @@ void receive_query(struct listener *listen, time_t now)
log_query_mysockaddr(F_QUERY | F_FORWARD, daemon->namebuff,
&source_addr, types);
#ifdef HAVE_CONNTRACK
is_single_query = 1;
#endif
#ifdef HAVE_AUTH
/* find queries for zones we're authoritative for, and answer them directly */
@@ -1454,20 +1533,50 @@ void receive_query(struct listener *listen, time_t now)
udp_size = PACKETSZ; /* Sanity check - can't reduce below default. RFC 6891 6.2.3 */
}
#ifdef HAVE_CONNTRACK
#ifdef HAVE_AUTH
if (auth_dns)
if (!auth_dns || local_auth)
#endif
if (option_bool(OPT_CMARK_ALST_EN) && have_mark && ((u32)mark & daemon->allowlist_mask))
allowed = is_query_allowed_for_mark((u32)mark, is_single_query ? daemon->namebuff : NULL);
#endif
if (0);
#ifdef HAVE_CONNTRACK
else if (!allowed)
{
m = answer_disallowed(header, (size_t)n, (u32)mark, is_single_query ? daemon->namebuff : NULL);
if (have_pseudoheader && m != 0)
m = add_pseudoheader(header, m, ((unsigned char *) header) + udp_size, daemon->edns_pktsz, 0, NULL, 0, do_bit, 0);
if (m >= 1)
{
send_from(listen->fd, option_bool(OPT_NOWILD) || option_bool(OPT_CLEVERBIND),
(char *)header, m, &source_addr, &dst_addr, if_index);
daemon->metrics[METRIC_DNS_LOCAL_ANSWERED]++;
}
}
#endif
#ifdef HAVE_AUTH
else if (auth_dns)
{
m = answer_auth(header, ((char *) header) + udp_size, (size_t)n, now, &source_addr,
local_auth, do_bit, have_pseudoheader);
if (m >= 1)
{
#if defined(HAVE_CONNTRACK) && defined(HAVE_UBUS)
if (local_auth)
if (option_bool(OPT_CMARK_ALST_EN) && have_mark && ((u32)mark & daemon->allowlist_mask))
report_addresses(header, m, mark);
#endif
send_from(listen->fd, option_bool(OPT_NOWILD) || option_bool(OPT_CLEVERBIND),
(char *)header, m, &source_addr, &dst_addr, if_index);
daemon->metrics[METRIC_DNS_AUTH_ANSWERED]++;
}
}
else
#endif
else
{
int ad_reqd = do_bit;
/* RFC 6840 5.7 */
@@ -1479,6 +1588,10 @@ void receive_query(struct listener *listen, time_t now)
if (m >= 1)
{
#if defined(HAVE_CONNTRACK) && defined(HAVE_UBUS)
if (option_bool(OPT_CMARK_ALST_EN) && have_mark && ((u32)mark & daemon->allowlist_mask))
report_addresses(header, m, mark);
#endif
send_from(listen->fd, option_bool(OPT_NOWILD) || option_bool(OPT_CLEVERBIND),
(char *)header, m, &source_addr, &dst_addr, if_index);
daemon->metrics[METRIC_DNS_LOCAL_ANSWERED]++;
@@ -1685,6 +1798,9 @@ unsigned char *tcp_request(int confd, time_t now,
{
size_t size = 0;
int norebind;
#ifdef HAVE_CONNTRACK
int is_single_query = 0, allowed = 1;
#endif
#ifdef HAVE_AUTH
int local_auth = 0;
#endif
@@ -1716,7 +1832,7 @@ unsigned char *tcp_request(int confd, time_t now,
#ifdef HAVE_CONNTRACK
/* Get connection mark of incoming query to set on outgoing connections. */
if (option_bool(OPT_CONNTRACK))
if (option_bool(OPT_CONNTRACK) || option_bool(OPT_CMARK_ALST_EN))
{
union all_addr local;
@@ -1796,6 +1912,10 @@ unsigned char *tcp_request(int confd, time_t now,
log_query_mysockaddr(F_QUERY | F_FORWARD, daemon->namebuff,
&peer_addr, types);
#ifdef HAVE_CONNTRACK
is_single_query = 1;
#endif
#ifdef HAVE_AUTH
/* find queries for zones we're authoritative for, and answer them directly */
if (!auth_dns && !option_bool(OPT_LOCALISE))
@@ -1829,13 +1949,31 @@ unsigned char *tcp_request(int confd, time_t now,
if (flags & 0x8000)
do_bit = 1; /* do bit */
}
#ifdef HAVE_CONNTRACK
#ifdef HAVE_AUTH
if (auth_dns)
if (!auth_dns || local_auth)
#endif
if (option_bool(OPT_CMARK_ALST_EN) && have_mark && ((u32)mark & daemon->allowlist_mask))
allowed = is_query_allowed_for_mark((u32)mark, is_single_query ? daemon->namebuff : NULL);
#endif
if (0);
#ifdef HAVE_CONNTRACK
else if (!allowed)
{
m = answer_disallowed(header, size, (u32)mark, is_single_query ? daemon->namebuff : NULL);
if (have_pseudoheader && m != 0)
m = add_pseudoheader(header, m, ((unsigned char *) header) + 65536, daemon->edns_pktsz, 0, NULL, 0, do_bit, 0);
}
#endif
#ifdef HAVE_AUTH
else if (auth_dns)
m = answer_auth(header, ((char *) header) + 65536, (size_t)size, now, &peer_addr,
local_auth, do_bit, have_pseudoheader);
else
#endif
else
{
int ad_reqd = do_bit;
/* RFC 6840 5.7 */
@@ -1950,7 +2088,8 @@ unsigned char *tcp_request(int confd, time_t now,
/* In case of local answer or no connections made. */
if (m == 0)
{
if (!(m = make_local_answer(flags, gotname, size, header, daemon->namebuff, first, last)))
if (!(m = make_local_answer(flags, gotname, size, header, daemon->namebuff,
((char *) header) + 65536, first, last)))
break;
if (have_pseudoheader)
@@ -1961,6 +2100,13 @@ unsigned char *tcp_request(int confd, time_t now,
*length = htons(m);
#if defined(HAVE_CONNTRACK) && defined(HAVE_UBUS)
#ifdef HAVE_AUTH
if (!auth_dns || local_auth)
#endif
if (option_bool(OPT_CMARK_ALST_EN) && have_mark && ((u32)mark & daemon->allowlist_mask))
report_addresses(header, m, mark);
#endif
if (!read_write(confd, packet, m + sizeof(u16), 0))
break;
}

View File

@@ -1617,7 +1617,7 @@ void add_update_server(int flags,
serv->flags = flags;
serv->domain = domain_str;
serv->domain_len = strlen(domain_str);
if (!(flags & SERV_IS_LOCAL))
{

View File

@@ -171,6 +171,8 @@ struct myoption {
#define LOPT_DYNHOST 362
#define LOPT_LOG_DEBUG 363
#define LOPT_UMBRELLA 364
#define LOPT_CMARK_ALST_EN 365
#define LOPT_CMARK_ALST 366
#ifdef HAVE_GETOPT_LONG
static const struct option opts[] =
@@ -324,6 +326,8 @@ static const struct myoption opts[] =
{ "auth-sec-servers", 1, 0, LOPT_AUTHSFS },
{ "auth-peer", 1, 0, LOPT_AUTHPEER },
{ "ipset", 1, 0, LOPT_IPSET },
{ "connmark-allowlist-enable", 2, 0, LOPT_CMARK_ALST_EN },
{ "connmark-allowlist", 1, 0, LOPT_CMARK_ALST },
{ "synth-domain", 1, 0, LOPT_SYNTH },
{ "dnssec", 0, 0, LOPT_SEC_VALID },
{ "trust-anchor", 1, 0, LOPT_TRUST_ANCHOR },
@@ -508,6 +512,8 @@ static struct {
{ LOPT_AUTHSFS, ARG_DUP, "<NS>[,<NS>...]", gettext_noop("Secondary authoritative nameservers for forward domains"), NULL },
{ LOPT_AUTHPEER, ARG_DUP, "<ipaddr>[,<ipaddr>...]", gettext_noop("Peers which are allowed to do zone transfer"), NULL },
{ LOPT_IPSET, ARG_DUP, "/<domain>[/<domain>...]/<ipset>...", gettext_noop("Specify ipsets to which matching domains should be added"), NULL },
{ LOPT_CMARK_ALST_EN, ARG_ONE, "[=<mask>]", gettext_noop("Enable filtering of DNS queries with connection-track marks."), NULL },
{ LOPT_CMARK_ALST, ARG_DUP, "<connmark>[/<mask>][,<pattern>[/<pattern>...]]", gettext_noop("Set allowed DNS patterns for a connection-track mark."), NULL },
{ LOPT_SYNTH, ARG_DUP, "<domain>,<range>,[<prefix>]", gettext_noop("Specify a domain and address range for synthesised names"), NULL },
{ LOPT_SEC_VALID, OPT_DNSSEC_VALID, NULL, gettext_noop("Activate DNSSEC validation"), NULL },
{ LOPT_TRUST_ANCHOR, ARG_DUP, "<domain>,[<class>],...", gettext_noop("Specify trust anchor key digest."), NULL },
@@ -685,13 +691,16 @@ static int atoi_check(char *a, int *res)
static int strtoul_check(char *a, u32 *res)
{
unsigned long x;
if (!numeric_check(a))
return 0;
*res = strtoul(a, NULL, 10);
if (errno == ERANGE) {
x = strtoul(a, NULL, 10);
if (errno || x > UINT32_MAX) {
errno = 0;
return 0;
}
*res = (u32)x;
return 1;
}
@@ -812,7 +821,7 @@ static char *parse_mysockaddr(char *arg, union mysockaddr *addr)
return NULL;
}
char *parse_server(char *arg, union mysockaddr *addr, union mysockaddr *source_addr, char *interface, int *flags)
char *parse_server(char *arg, union mysockaddr *addr, union mysockaddr *source_addr, char *interface, u16 *flags)
{
int source_port = 0, serv_port = NAMESERVER_PORT;
char *portno, *source;
@@ -2617,7 +2626,7 @@ static int one_opt(int option, char *arg, char *errstr, char *gen_err, int comma
comma = split_chr(arg, '/');
new = opt_malloc(sizeof(struct serv_local));
new->domain = opt_string_alloc(arg);
new->flags = strlen(arg);
new->domain_len = strlen(arg);
new->next = daemon->no_rebind;
daemon->no_rebind = new;
arg = comma;
@@ -2634,7 +2643,7 @@ static int one_opt(int option, char *arg, char *errstr, char *gen_err, int comma
size_t size;
char *lastdomain = NULL, *domain = "";
char *alloc_domain;
int flags = 0;
u16 flags = 0;
char *err;
struct in_addr addr4;
struct in6_addr addr6;
@@ -2647,8 +2656,11 @@ static int one_opt(int option, char *arg, char *errstr, char *gen_err, int comma
char *last;
arg++;
domain = lastdomain = arg;
/* elide leading dots - they are implied in the search algorithm */
while (*arg == '.') arg++;
domain = lastdomain = arg;
while ((last = split_chr(arg, '/')))
{
lastdomain = arg;
@@ -2730,6 +2742,7 @@ static int one_opt(int option, char *arg, char *errstr, char *gen_err, int comma
}
new->domain = alloc_domain;
new->domain_len = strlen(alloc_domain);
/* server=//1.2.3.4 is special. */
if (strlen(domain) == 0 && lastdomain)
@@ -2751,6 +2764,8 @@ static int one_opt(int option, char *arg, char *errstr, char *gen_err, int comma
new = opt_malloc(size);
memcpy(new, last, size);
new->domain = alloc_domain;
new->domain_len = strlen(alloc_domain);
if (flags & (SERV_USE_RESOLV | SERV_LITERAL_ADDRESS))
{
new->next = daemon->local_domains;
@@ -2877,6 +2892,135 @@ static int one_opt(int option, char *arg, char *errstr, char *gen_err, int comma
}
#endif
case LOPT_CMARK_ALST_EN: /* --connmark-allowlist-enable */
#ifndef HAVE_CONNTRACK
ret_err(_("recompile with HAVE_CONNTRACK defined to enable connmark-allowlist directives"));
break;
#else
{
u32 mask = UINT32_MAX;
if (arg)
if (!strtoul_check(arg, &mask) || mask < 1)
ret_err(gen_err);
set_option_bool(OPT_CMARK_ALST_EN);
daemon->allowlist_mask = mask;
break;
}
#endif
case LOPT_CMARK_ALST: /* --connmark-allowlist */
#ifndef HAVE_CONNTRACK
ret_err(_("recompile with HAVE_CONNTRACK defined to enable connmark-allowlist directives"));
break;
#else
{
struct allowlist *allowlists;
char **patterns, **patterns_pos;
u32 mark, mask = UINT32_MAX;
size_t num_patterns = 0;
char *c, *m = NULL;
char *separator;
unhide_metas(arg);
if (!arg)
ret_err(gen_err);
c = arg;
if (*c < '0' || *c > '9')
ret_err(gen_err);
while (*c && *c != ',')
{
if (*c == '/')
{
if (m)
ret_err(gen_err);
*c = '\0';
m = ++c;
}
if (*c < '0' || *c > '9')
ret_err(gen_err);
c++;
}
separator = c;
if (!*separator)
break;
while (c && *c)
{
char *end = strchr(++c, '/');
if (end)
*end = '\0';
if (strcmp(c, "*") && !is_valid_dns_name_pattern(c))
ret_err(gen_err);
if (end)
*end = '/';
if (num_patterns >= UINT16_MAX - 1)
ret_err(gen_err);
num_patterns++;
c = end;
}
*separator = '\0';
if (!strtoul_check(arg, &mark) || mark < 1 || mark > UINT32_MAX)
ret_err(gen_err);
if (m)
if (!strtoul_check(m, &mask) || mask < 1 || mask > UINT32_MAX || (mark & ~mask))
ret_err(gen_err);
if (num_patterns)
*separator = ',';
for (allowlists = daemon->allowlists; allowlists; allowlists = allowlists->next)
if (allowlists->mark == mark && allowlists->mask == mask)
ret_err(gen_err);
patterns = opt_malloc((num_patterns + 1) * sizeof(char *));
if (!patterns)
goto fail_cmark_allowlist;
patterns_pos = patterns;
c = separator;
while (c && *c)
{
char *end = strchr(++c, '/');
if (end)
*end = '\0';
if (!(*patterns_pos++ = opt_string_alloc(c)))
goto fail_cmark_allowlist;
if (end)
*end = '/';
c = end;
}
*patterns_pos++ = NULL;
allowlists = opt_malloc(sizeof(struct allowlist));
if (!allowlists)
goto fail_cmark_allowlist;
memset(allowlists, 0, sizeof(struct allowlist));
allowlists->mark = mark;
allowlists->mask = mask;
allowlists->patterns = patterns;
allowlists->next = daemon->allowlists;
daemon->allowlists = allowlists;
break;
fail_cmark_allowlist:
if (patterns)
{
for (patterns_pos = patterns; *patterns_pos; patterns_pos++)
{
free(*patterns_pos);
*patterns_pos = NULL;
}
free(patterns);
patterns = NULL;
}
if (allowlists)
{
free(allowlists);
allowlists = NULL;
}
ret_err(gen_err);
}
#endif
case 'c': /* --cache-size */
{
int size;

386
src/pattern.c Normal file
View File

@@ -0,0 +1,386 @@
/* dnsmasq is Copyright (c) 2000-2021 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; version 2 dated June, 1991, or
(at your option) version 3 dated 29 June, 2007.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "dnsmasq.h"
#ifdef HAVE_CONNTRACK
#define LOG(...) \
do { \
my_syslog(LOG_WARNING, __VA_ARGS__); \
} while (0)
#define ASSERT(condition) \
do { \
if (!(condition)) \
LOG("[pattern.c:%d] Assertion failure: %s", __LINE__, #condition); \
} while (0)
/**
* Determines whether a given string value matches against a glob pattern
* which may contain zero-or-more-character wildcards denoted by '*'.
*
* Based on "Glob Matching Can Be Simple And Fast Too" by Russ Cox,
* See https://research.swtch.com/glob
*
* @param value A string value.
* @param num_value_bytes The number of bytes of the string value.
* @param pattern A glob pattern.
* @param num_pattern_bytes The number of bytes of the glob pattern.
*
* @return 1 If the provided value matches against the glob pattern.
* @return 0 Otherwise.
*/
static int is_string_matching_glob_pattern(
const char *value,
size_t num_value_bytes,
const char *pattern,
size_t num_pattern_bytes)
{
ASSERT(value);
ASSERT(pattern);
size_t value_index = 0;
size_t next_value_index = 0;
size_t pattern_index = 0;
size_t next_pattern_index = 0;
while (value_index < num_value_bytes || pattern_index < num_pattern_bytes)
{
if (pattern_index < num_pattern_bytes)
{
char pattern_character = pattern[pattern_index];
if ('a' <= pattern_character && pattern_character <= 'z')
pattern_character -= 'a' - 'A';
if (pattern_character == '*')
{
// zero-or-more-character wildcard
// Try to match at value_index, otherwise restart at value_index + 1 next.
next_pattern_index = pattern_index;
pattern_index++;
if (value_index < num_value_bytes)
next_value_index = value_index + 1;
else
next_value_index = 0;
continue;
}
else
{
// ordinary character
if (value_index < num_value_bytes)
{
char value_character = value[value_index];
if ('a' <= value_character && value_character <= 'z')
value_character -= 'a' - 'A';
if (value_character == pattern_character)
{
pattern_index++;
value_index++;
continue;
}
}
}
}
if (next_value_index)
{
pattern_index = next_pattern_index;
value_index = next_value_index;
continue;
}
return 0;
}
return 1;
}
/**
* Determines whether a given string value represents a valid DNS name.
*
* - DNS names must adhere to RFC 1123: 1 to 253 characters in length, consisting of a sequence of labels
* delimited by dots ("."). Each label must be 1 to 63 characters in length, contain only
* ASCII letters ("a"-"Z"), digits ("0"-"9"), or hyphens ("-") and must not start or end with a hyphen.
*
* - A valid name must be fully qualified, i.e., consist of at least two labels.
* The final label must not be fully numeric, and must not be the "local" pseudo-TLD.
*
* - Examples:
* Valid: "example.com"
* Invalid: "ipcamera", "ipcamera.local", "8.8.8.8"
*
* @param value A string value.
*
* @return 1 If the provided string value is a valid DNS name.
* @return 0 Otherwise.
*/
int is_valid_dns_name(const char *value)
{
ASSERT(value);
size_t num_bytes = 0;
size_t num_labels = 0;
const char *label = NULL;
int is_label_numeric = 1;
for (const char *c = value;; c++)
{
if (*c &&
*c != '-' && *c != '.' &&
(*c < '0' || *c > '9') &&
(*c < 'A' || *c > 'Z') &&
(*c < 'a' || *c > 'z'))
{
LOG("Invalid DNS name: Invalid character %c.", *c);
return 0;
}
if (*c)
num_bytes++;
if (!label)
{
if (!*c || *c == '.')
{
LOG("Invalid DNS name: Empty label.");
return 0;
}
if (*c == '-')
{
LOG("Invalid DNS name: Label starts with hyphen.");
return 0;
}
label = c;
}
if (*c && *c != '.')
{
if (*c < '0' || *c > '9')
is_label_numeric = 0;
}
else
{
if (c[-1] == '-')
{
LOG("Invalid DNS name: Label ends with hyphen.");
return 0;
}
size_t num_label_bytes = (size_t) (c - label);
if (num_label_bytes > 63)
{
LOG("Invalid DNS name: Label is too long (%zu).", num_label_bytes);
return 0;
}
num_labels++;
if (!*c)
{
if (num_labels < 2)
{
LOG("Invalid DNS name: Not enough labels (%zu).", num_labels);
return 0;
}
if (is_label_numeric)
{
LOG("Invalid DNS name: Final label is fully numeric.");
return 0;
}
if (num_label_bytes == 5 &&
(label[0] == 'l' || label[0] == 'L') &&
(label[1] == 'o' || label[1] == 'O') &&
(label[2] == 'c' || label[2] == 'C') &&
(label[3] == 'a' || label[3] == 'A') &&
(label[4] == 'l' || label[4] == 'L'))
{
LOG("Invalid DNS name: \"local\" pseudo-TLD.");
return 0;
}
if (num_bytes < 1 || num_bytes > 253)
{
LOG("DNS name has invalid length (%zu).", num_bytes);
return 0;
}
return 1;
}
label = NULL;
is_label_numeric = 1;
}
}
}
/**
* Determines whether a given string value represents a valid DNS name pattern.
*
* - DNS names must adhere to RFC 1123: 1 to 253 characters in length, consisting of a sequence of labels
* delimited by dots ("."). Each label must be 1 to 63 characters in length, contain only
* ASCII letters ("a"-"Z"), digits ("0"-"9"), or hyphens ("-") and must not start or end with a hyphen.
*
* - Patterns follow the syntax of DNS names, but additionally allow the wildcard character "*" to be used up to
* twice per label to match 0 or more characters within that label. Note that the wildcard never matches a dot
* (e.g., "*.example.com" matches "api.example.com" but not "api.us.example.com").
*
* - A valid name or pattern must be fully qualified, i.e., consist of at least two labels.
* The final label must not be fully numeric, and must not be the "local" pseudo-TLD.
* A pattern must end with at least two literal (non-wildcard) labels.
*
* - Examples:
* Valid: "example.com", "*.example.com", "video*.example.com", "api*.*.example.com", "*-prod-*.example.com"
* Invalid: "ipcamera", "ipcamera.local", "*", "*.com", "8.8.8.8"
*
* @param value A string value.
*
* @return 1 If the provided string value is a valid DNS name pattern.
* @return 0 Otherwise.
*/
int is_valid_dns_name_pattern(const char *value)
{
ASSERT(value);
size_t num_bytes = 0;
size_t num_labels = 0;
const char *label = NULL;
int is_label_numeric = 1;
size_t num_wildcards = 0;
int previous_label_has_wildcard = 1;
for (const char *c = value;; c++)
{
if (*c &&
*c != '*' && // Wildcard.
*c != '-' && *c != '.' &&
(*c < '0' || *c > '9') &&
(*c < 'A' || *c > 'Z') &&
(*c < 'a' || *c > 'z'))
{
LOG("Invalid DNS name pattern: Invalid character %c.", *c);
return 0;
}
if (*c && *c != '*')
num_bytes++;
if (!label)
{
if (!*c || *c == '.')
{
LOG("Invalid DNS name pattern: Empty label.");
return 0;
}
if (*c == '-')
{
LOG("Invalid DNS name pattern: Label starts with hyphen.");
return 0;
}
label = c;
}
if (*c && *c != '.')
{
if (*c < '0' || *c > '9')
is_label_numeric = 0;
if (*c == '*')
{
if (num_wildcards >= 2)
{
LOG("Invalid DNS name pattern: Wildcard character used more than twice per label.");
return 0;
}
num_wildcards++;
}
}
else
{
if (c[-1] == '-')
{
LOG("Invalid DNS name pattern: Label ends with hyphen.");
return 0;
}
size_t num_label_bytes = (size_t) (c - label) - num_wildcards;
if (num_label_bytes > 63)
{
LOG("Invalid DNS name pattern: Label is too long (%zu).", num_label_bytes);
return 0;
}
num_labels++;
if (!*c)
{
if (num_labels < 2)
{
LOG("Invalid DNS name pattern: Not enough labels (%zu).", num_labels);
return 0;
}
if (num_wildcards != 0 || previous_label_has_wildcard)
{
LOG("Invalid DNS name pattern: Wildcard within final two labels.");
return 0;
}
if (is_label_numeric)
{
LOG("Invalid DNS name pattern: Final label is fully numeric.");
return 0;
}
if (num_label_bytes == 5 &&
(label[0] == 'l' || label[0] == 'L') &&
(label[1] == 'o' || label[1] == 'O') &&
(label[2] == 'c' || label[2] == 'C') &&
(label[3] == 'a' || label[3] == 'A') &&
(label[4] == 'l' || label[4] == 'L'))
{
LOG("Invalid DNS name pattern: \"local\" pseudo-TLD.");
return 0;
}
if (num_bytes < 1 || num_bytes > 253)
{
LOG("DNS name pattern has invalid length after removing wildcards (%zu).", num_bytes);
return 0;
}
return 1;
}
label = NULL;
is_label_numeric = 1;
previous_label_has_wildcard = num_wildcards != 0;
num_wildcards = 0;
}
}
}
/**
* Determines whether a given DNS name matches against a DNS name pattern.
*
* @param name A valid DNS name.
* @param pattern A valid DNS name pattern.
*
* @return 1 If the provided DNS name matches against the DNS name pattern.
* @return 0 Otherwise.
*/
int is_dns_name_matching_pattern(const char *name, const char *pattern)
{
ASSERT(name);
ASSERT(is_valid_dns_name(name));
ASSERT(pattern);
ASSERT(is_valid_dns_name_pattern(pattern));
const char *n = name;
const char *p = pattern;
do {
const char *name_label = n;
while (*n && *n != '.')
n++;
const char *pattern_label = p;
while (*p && *p != '.')
p++;
if (!is_string_matching_glob_pattern(
name_label, (size_t) (n - name_label),
pattern_label, (size_t) (p - pattern_label)))
break;
if (*n)
n++;
if (*p)
p++;
} while (*n && *p);
return !*n && !*p;
}
#endif

View File

@@ -884,6 +884,92 @@ int extract_addresses(struct dns_header *header, size_t qlen, char *name, time_t
return 0;
}
#if defined(HAVE_CONNTRACK) && defined(HAVE_UBUS)
/* Don't pass control chars and weird escapes to UBus. */
static int safe_name(char *name)
{
unsigned char *r;
for (r = (unsigned char *)name; *r; r++)
if (!isprint((int)*r))
return 0;
return 1;
}
void report_addresses(struct dns_header *header, size_t len, u32 mark)
{
unsigned char *p, *endrr;
int i;
unsigned long attl;
struct allowlist *allowlists;
char **pattern_pos;
if (RCODE(header) != NOERROR)
return;
for (allowlists = daemon->allowlists; allowlists; allowlists = allowlists->next)
if (allowlists->mark == (mark & daemon->allowlist_mask & allowlists->mask))
for (pattern_pos = allowlists->patterns; *pattern_pos; pattern_pos++)
if (!strcmp(*pattern_pos, "*"))
return;
if (!(p = skip_questions(header, len)))
return;
for (i = ntohs(header->ancount); i != 0; i--)
{
int aqtype, aqclass, ardlen;
if (!extract_name(header, len, &p, daemon->namebuff, 1, 10))
return;
if (!CHECK_LEN(header, p, len, 10))
return;
GETSHORT(aqtype, p);
GETSHORT(aqclass, p);
GETLONG(attl, p);
GETSHORT(ardlen, p);
if (!CHECK_LEN(header, p, len, ardlen))
return;
endrr = p+ardlen;
if (aqclass == C_IN)
{
if (aqtype == T_CNAME)
{
if (!extract_name(header, len, &p, daemon->workspacename, 1, 0))
return;
if (safe_name(daemon->namebuff) && safe_name(daemon->workspacename))
ubus_event_bcast_connmark_allowlist_resolved(mark, daemon->namebuff, daemon->workspacename, attl);
}
if (aqtype == T_A)
{
struct in_addr addr;
char ip[INET_ADDRSTRLEN];
if (ardlen != INADDRSZ)
return;
memcpy(&addr, p, ardlen);
if (inet_ntop(AF_INET, &addr, ip, sizeof ip) && safe_name(daemon->namebuff))
ubus_event_bcast_connmark_allowlist_resolved(mark, daemon->namebuff, ip, attl);
}
else if (aqtype == T_AAAA)
{
struct in6_addr addr;
char ip[INET6_ADDRSTRLEN];
if (ardlen != IN6ADDRSZ)
return;
memcpy(&addr, p, ardlen);
if (inet_ntop(AF_INET6, &addr, ip, sizeof ip) && safe_name(daemon->namebuff))
ubus_event_bcast_connmark_allowlist_resolved(mark, daemon->namebuff, ip, attl);
}
}
p = endrr;
}
}
#endif
/* If the packet holds exactly one query
return F_IPV4 or F_IPV6 and leave the name from the query in name */
unsigned int extract_request(struct dns_header *header, size_t qlen, char *name, unsigned short *typep)
@@ -896,7 +982,8 @@ unsigned int extract_request(struct dns_header *header, size_t qlen, char *name,
*name = 0; /* return empty name if no query found. */
if (ntohs(header->qdcount) != 1 || OPCODE(header) != QUERY)
if (ntohs(header->qdcount) != 1 || OPCODE(header) != QUERY ||
ntohs(header->ancount) != 0 || ntohs(header->nscount) != 0)
return 0; /* must be exactly one query. */
if (!extract_name(header, qlen, &p, name, 1, 4))

View File

@@ -28,10 +28,38 @@ static int ubus_handle_metrics(struct ubus_context *ctx, struct ubus_object *obj
struct ubus_request_data *req, const char *method,
struct blob_attr *msg);
#ifdef HAVE_CONNTRACK
enum {
SET_CONNMARK_ALLOWLIST_MARK,
SET_CONNMARK_ALLOWLIST_MASK,
SET_CONNMARK_ALLOWLIST_PATTERNS
};
static const struct blobmsg_policy set_connmark_allowlist_policy[] = {
[SET_CONNMARK_ALLOWLIST_MARK] = {
.name = "mark",
.type = BLOBMSG_TYPE_INT32
},
[SET_CONNMARK_ALLOWLIST_MASK] = {
.name = "mask",
.type = BLOBMSG_TYPE_INT32
},
[SET_CONNMARK_ALLOWLIST_PATTERNS] = {
.name = "patterns",
.type = BLOBMSG_TYPE_ARRAY
}
};
static int ubus_handle_set_connmark_allowlist(struct ubus_context *ctx, struct ubus_object *obj,
struct ubus_request_data *req, const char *method,
struct blob_attr *msg);
#endif
static void ubus_subscribe_cb(struct ubus_context *ctx, struct ubus_object *obj);
static const struct ubus_method ubus_object_methods[] = {
UBUS_METHOD_NOARG("metrics", ubus_handle_metrics),
#ifdef HAVE_CONNTRACK
UBUS_METHOD("set_connmark_allowlist", ubus_handle_set_connmark_allowlist, set_connmark_allowlist_policy),
#endif
};
static struct ubus_object_type ubus_object_type =
@@ -89,7 +117,7 @@ char *ubus_init()
if (ret)
{
ubus_destroy(ubus);
return ubus_strerror(ret);
return (char *)ubus_strerror(ret);
}
ubus->connection_lost = ubus_disconnect_cb;
@@ -163,6 +191,122 @@ static int ubus_handle_metrics(struct ubus_context *ctx, struct ubus_object *obj
return ubus_send_reply(ctx, req, b.head);
}
#ifdef HAVE_CONNTRACK
static int ubus_handle_set_connmark_allowlist(struct ubus_context *ctx, struct ubus_object *obj,
struct ubus_request_data *req, const char *method,
struct blob_attr *msg)
{
const struct blobmsg_policy *policy = set_connmark_allowlist_policy;
size_t policy_len = countof(set_connmark_allowlist_policy);
struct allowlist *allowlists = NULL, **allowlists_pos;
char **patterns = NULL, **patterns_pos;
u32 mark, mask = UINT32_MAX;
size_t num_patterns = 0;
struct blob_attr *tb[policy_len];
struct blob_attr *attr;
if (blobmsg_parse(policy, policy_len, tb, blob_data(msg), blob_len(msg)))
return UBUS_STATUS_INVALID_ARGUMENT;
if (!tb[SET_CONNMARK_ALLOWLIST_MARK])
return UBUS_STATUS_INVALID_ARGUMENT;
mark = blobmsg_get_u32(tb[SET_CONNMARK_ALLOWLIST_MARK]);
if (!mark)
return UBUS_STATUS_INVALID_ARGUMENT;
if (tb[SET_CONNMARK_ALLOWLIST_MASK])
{
mask = blobmsg_get_u32(tb[SET_CONNMARK_ALLOWLIST_MASK]);
if (!mask || (mark & ~mask))
return UBUS_STATUS_INVALID_ARGUMENT;
}
if (tb[SET_CONNMARK_ALLOWLIST_PATTERNS])
{
struct blob_attr *head = blobmsg_data(tb[SET_CONNMARK_ALLOWLIST_PATTERNS]);
size_t len = blobmsg_data_len(tb[SET_CONNMARK_ALLOWLIST_PATTERNS]);
__blob_for_each_attr(attr, head, len)
{
char *pattern;
if (blob_id(attr) != BLOBMSG_TYPE_STRING)
return UBUS_STATUS_INVALID_ARGUMENT;
if (!(pattern = blobmsg_get_string(attr)))
return UBUS_STATUS_INVALID_ARGUMENT;
if (strcmp(pattern, "*") && !is_valid_dns_name_pattern(pattern))
return UBUS_STATUS_INVALID_ARGUMENT;
num_patterns++;
}
}
for (allowlists_pos = &daemon->allowlists; *allowlists_pos; allowlists_pos = &(*allowlists_pos)->next)
if ((*allowlists_pos)->mark == mark && (*allowlists_pos)->mask == mask)
{
struct allowlist *allowlists_next = (*allowlists_pos)->next;
for (patterns_pos = (*allowlists_pos)->patterns; *patterns_pos; patterns_pos++)
{
free(*patterns_pos);
*patterns_pos = NULL;
}
free((*allowlists_pos)->patterns);
(*allowlists_pos)->patterns = NULL;
free(*allowlists_pos);
*allowlists_pos = allowlists_next;
break;
}
if (!num_patterns)
return UBUS_STATUS_OK;
patterns = whine_malloc((num_patterns + 1) * sizeof(char *));
if (!patterns)
goto fail;
patterns_pos = patterns;
if (tb[SET_CONNMARK_ALLOWLIST_PATTERNS])
{
struct blob_attr *head = blobmsg_data(tb[SET_CONNMARK_ALLOWLIST_PATTERNS]);
size_t len = blobmsg_data_len(tb[SET_CONNMARK_ALLOWLIST_PATTERNS]);
__blob_for_each_attr(attr, head, len)
{
char *pattern;
if (!(pattern = blobmsg_get_string(attr)))
goto fail;
if (!(*patterns_pos = whine_malloc(strlen(pattern) + 1)))
goto fail;
strcpy(*patterns_pos++, pattern);
}
}
allowlists = whine_malloc(sizeof(struct allowlist));
if (!allowlists)
goto fail;
memset(allowlists, 0, sizeof(struct allowlist));
allowlists->mark = mark;
allowlists->mask = mask;
allowlists->patterns = patterns;
allowlists->next = daemon->allowlists;
daemon->allowlists = allowlists;
return UBUS_STATUS_OK;
fail:
if (patterns)
{
for (patterns_pos = patterns; *patterns_pos; patterns_pos++)
{
free(*patterns_pos);
*patterns_pos = NULL;
}
free(patterns);
patterns = NULL;
}
if (allowlists)
{
free(allowlists);
allowlists = NULL;
}
return UBUS_STATUS_UNKNOWN_ERROR;
}
#endif
void ubus_event_bcast(const char *type, const char *mac, const char *ip, const char *name, const char *interface)
{
struct ubus_context *ubus = (struct ubus_context *)daemon->ubus;
@@ -182,9 +326,47 @@ void ubus_event_bcast(const char *type, const char *mac, const char *ip, const c
blobmsg_add_string(&b, "interface", interface);
ret = ubus_notify(ubus, &ubus_object, type, b.head, -1);
if (!ret)
if (ret)
my_syslog(LOG_ERR, _("Failed to send UBus event: %s"), ubus_strerror(ret));
}
#ifdef HAVE_CONNTRACK
void ubus_event_bcast_connmark_allowlist_refused(u32 mark, const char *name)
{
struct ubus_context *ubus = (struct ubus_context *)daemon->ubus;
int ret;
if (!ubus || !notify)
return;
blob_buf_init(&b, 0);
blobmsg_add_u32(&b, "mark", mark);
blobmsg_add_string(&b, "name", name);
ret = ubus_notify(ubus, &ubus_object, "connmark-allowlist.refused", b.head, -1);
if (ret)
my_syslog(LOG_ERR, _("Failed to send UBus event: %s"), ubus_strerror(ret));
}
void ubus_event_bcast_connmark_allowlist_resolved(u32 mark, const char *name, const char *value, u32 ttl)
{
struct ubus_context *ubus = (struct ubus_context *)daemon->ubus;
int ret;
if (!ubus || !notify)
return;
blob_buf_init(&b, 0);
blobmsg_add_u32(&b, "mark", mark);
blobmsg_add_string(&b, "name", name);
blobmsg_add_string(&b, "value", value);
blobmsg_add_u32(&b, "ttl", ttl);
ret = ubus_notify(ubus, &ubus_object, "connmark-allowlist.resolved", b.head, /* timeout: */ 1000);
if (ret)
my_syslog(LOG_ERR, _("Failed to send UBus event: %s"), ubus_strerror(ret));
}
#endif
#endif /* HAVE_UBUS */