From 5e95c16c322afde473c2838d64700e6363df3e40 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Thu, 1 Jul 2021 22:28:24 +0100 Subject: [PATCH] Allow wildcards in domain patterns. Domain patterns in --address, --server and --local have, for many years, matched complete labels only, so --server=/google.com/1.2.3.4 will apply to google.com and www.google.com but NOT supergoogle.com This commit introduces an optional '*' at the LHS of the domain string which changes this behaviour so as to include substring matches _within_ labels. So, --server=/*google.com/1.2.3.4 applies to google.com, www.google.com AND supergoogle.com. --- man/dnsmasq.8 | 15 ++++++-- src/dnsmasq.h | 3 +- src/domain-match.c | 87 +++++++++++++++++++++++++++++++++++----------- src/network.c | 6 ++-- 4 files changed, 84 insertions(+), 27 deletions(-) diff --git a/man/dnsmasq.8 b/man/dnsmasq.8 index ea8457b..408eb1b 100644 --- a/man/dnsmasq.8 +++ b/man/dnsmasq.8 @@ -460,13 +460,22 @@ repeated domain or ipaddr parts as required. More specific domains take precedence over less specific domains, so: .B --server=/google.com/1.2.3.4 .B --server=/www.google.com/2.3.4.5 -will send queries for *.google.com to 1.2.3.4, except *www.google.com, -which will go to 2.3.4.5 +will send queries for google.com and gmail.google.com to 1.2.3.4, but www.google.com +will go to 2.3.4.5 + +Matching of domains is normally done on complete labels, so /google.com/ matches google.com and www.google.com +but NOT supergoogle.com. This can be overridden with a * at the start of a pattern only: /*google.com/ +will match google.com and www.google.com AND supergoogle.com. The non-wildcard form has priority, so +if /google.com/ and /*google.com/ are both specified then google.com and www.google.com will match /google.com/ +and /*google.com/ will only match supergoogle.com. + +For historical reasons, the pattern /.google.com/ is equivalent to /google.com/ if you wish to match any subdomain +of google.com but NOT google.com itself, use /*.google.com/ The special server address '#' means, "use the standard servers", so .B --server=/google.com/1.2.3.4 .B --server=/www.google.com/# -will send queries for *.google.com to 1.2.3.4, except *www.google.com which will +will send queries for google.com and its subdomains to 1.2.3.4, except www.google.com (and its subdomains) which will be forwarded as usual. Also permitted is a -S diff --git a/src/dnsmasq.h b/src/dnsmasq.h index 61be3df..81bd11e 100644 --- a/src/dnsmasq.h +++ b/src/dnsmasq.h @@ -537,7 +537,7 @@ union mysockaddr { #define SERV_WARNED_RECURSIVE 64 /* avoid warning spam */ #define SERV_FROM_DBUS 128 /* 1 if source is DBus */ #define SERV_MARK 256 /* for mark-and-delete and log code */ -/* #define SERV_COUNTED 512 /* workspace for log code */ +#define SERV_WILDCARD 512 /* domain has leading '*' */ #define SERV_USE_RESOLV 1024 /* forward this domain in the normal way */ #define SERV_FROM_RESOLV 2048 /* 1 for servers from resolv, 0 for command line. */ #define SERV_FROM_FILE 4096 /* read from --servers-file */ @@ -1103,6 +1103,7 @@ extern struct daemon { struct iname *if_names, *if_addrs, *if_except, *dhcp_except, *auth_peers, *tftp_interfaces; struct bogus_addr *bogus_addr, *ignore_addr; struct server *servers, *local_domains, **serverarray, *no_rebind; + int server_has_wildcard; int serverarraysz, serverarrayhwm; struct ipsets *ipsets; u32 allowlist_mask; diff --git a/src/domain-match.c b/src/domain-match.c index 4242d0a..9b9d682 100644 --- a/src/domain-match.c +++ b/src/domain-match.c @@ -32,11 +32,19 @@ void build_server_array(void) #ifdef HAVE_LOOP if (!(serv->flags & SERV_LOOP)) #endif - count++; + { + count++; + if (serv->flags & SERV_WILDCARD) + daemon->server_has_wildcard = 1; + } for (serv = daemon->local_domains; serv; serv = serv->next) - count++; - + { + count++; + if (serv->flags & SERV_WILDCARD) + daemon->server_has_wildcard = 1; + } + daemon->serverarraysz = count; if (count > daemon->serverarrayhwm) @@ -92,13 +100,13 @@ void build_server_array(void) reply of IPv4 or IPV6. return 0 if nothing found, 1 otherwise. */ -int lookup_domain(char *qdomain, int flags, int *lowout, int *highout) +int lookup_domain(char *domain, int flags, int *lowout, int *highout) { int rc, crop_query, nodots; ssize_t qlen; int try, high, low = 0; int nlow = 0, nhigh = 0; - char *cp; + char *cp, *qdomain = domain; /* may be no configured servers. */ if (daemon->serverarraysz == 0) @@ -178,16 +186,36 @@ int lookup_domain(char *qdomain, int flags, int *lowout, int *highout) if (rc == 0) { - /* We've matched a setting which says to use servers without a domain. - Continue the search with empty query */ - if (daemon->serverarray[try]->flags & SERV_USE_RESOLV) - crop_query = qlen; - else + int found = 1; + + if (daemon->server_has_wildcard) { - /* We have a match, but it may only be (say) an IPv6 address, and - if the query wasn't for an AAAA record, it's no good, and we need - to continue generalising */ - if (filter_servers(try, flags, &nlow, &nhigh)) + /* if we have example.com and *example.com we need to check against *example.com, + but the binary search may have found either. Use the fact that example.com is sorted before *example.com + We favour example.com in the case that both match (ie www.example.com) */ + while (try != 0 && order(qdomain, qlen, daemon->serverarray[try-1]) == 0) + try--; + + if (!(qdomain == domain || *qdomain == 0 || *(qdomain-1) == '.')) + { + while (try < daemon->serverarraysz-1 && order(qdomain, qlen, daemon->serverarray[try+1]) == 0) + try++; + + if (!(daemon->serverarray[try]->flags & SERV_WILDCARD)) + found = 0; + } + } + + if (found) + { + /* We've matched a setting which says to use servers without a domain. + Continue the search with empty query */ + if (daemon->serverarray[try]->flags & SERV_USE_RESOLV) + crop_query = qlen; + else if (filter_servers(try, flags, &nlow, &nhigh)) + /* We have a match, but it may only be (say) an IPv6 address, and + if the query wasn't for an AAAA record, it's no good, and we need + to continue generalising */ break; } } @@ -197,11 +225,13 @@ int lookup_domain(char *qdomain, int flags, int *lowout, int *highout) crop_query = 1; /* strip chars off the query based on the largest possible remaining match, - then continue to the start of the next label. */ + then continue to the start of the next label unless we have a wildcard + domain somewhere, in which case we have to go one at a time. */ qlen -= crop_query; qdomain += crop_query; - while (qlen > 0 && (*(qdomain-1) != '.')) - qlen--, qdomain++; + if (!daemon->server_has_wildcard) + while (qlen > 0 && (*(qdomain-1) != '.')) + qlen--, qdomain++; } /* domain has no dots, and we have at least one server configured to handle such, @@ -449,13 +479,22 @@ static int order(char *qdomain, size_t qlen, struct server *serv) static int order_servers(struct server *s1, struct server *s2) { + int rc; + /* need full comparison of dotless servers in order_qsort() and filter_servers() */ if (s1->flags & SERV_FOR_NODOTS) return (s2->flags & SERV_FOR_NODOTS) ? 0 : 1; - return order(s1->domain, s1->domain_len, s2); + if ((rc = order(s1->domain, s1->domain_len, s2)) != 0) + return rc; + + /* For identical domains, sort wildcard ones first */ + if (s1->flags & SERV_WILDCARD) + return (s2->flags & SERV_WILDCARD) ? 0 : 1; + + return (s2->flags & SERV_WILDCARD) ? -1 : 0; } static int order_qsort(const void *a, const void *b) @@ -540,8 +579,16 @@ int add_update_server(int flags, if (!domain || strlen(domain) == 0) alloc_domain = whine_malloc(1); - else if (!(alloc_domain = canonicalise((char *)domain, NULL))) - return 0; + else + { + if (*domain == '*') + { + domain++; + flags |= SERV_WILDCARD; + } + if (!(alloc_domain = canonicalise((char *)domain, NULL))) + return 0; + } /* See if there is a suitable candidate, and unmark only do this for forwarding servers, not diff --git a/src/network.c b/src/network.c index e189edd..2d77b47 100644 --- a/src/network.c +++ b/src/network.c @@ -1588,7 +1588,7 @@ void check_servers(int no_loop_check) if (strlen(serv->domain) != 0 || (serv->flags & SERV_FOR_NODOTS)) { - char *s1, *s2, *s3 = ""; + char *s1, *s2, *s3 = "", *s4 = ""; #ifdef HAVE_DNSSEC if (option_bool(OPT_DNSSEC_VALID) && !(serv->flags & SERV_DO_DNSSEC)) @@ -1599,9 +1599,9 @@ void check_servers(int no_loop_check) else if (strlen(serv->domain) == 0) s1 = _("default"), s2 = ""; else - s1 = _("domain"), s2 = serv->domain; + s1 = _("domain"), s2 = serv->domain, s4 = (serv->flags & SERV_WILDCARD) ? "*" : ""; - my_syslog(LOG_INFO, _("using nameserver %s#%d for %s %s %s"), daemon->namebuff, port, s1, s2, s3); + my_syslog(LOG_INFO, _("using nameserver %s#%d for %s %s%s %s"), daemon->namebuff, port, s1, s4, s2, s3); } #ifdef HAVE_LOOP else if (serv->flags & SERV_LOOP)