Index | Thread | Search

From:
Kirill A. Korinsky <kirill@korins.ky>
Subject:
Re: unwind: support wildcard in blacklist
To:
Otto Moerbeek <otto@drijf.net>, OpenBSD tech <tech@openbsd.org>, florian@openbsd.org
Date:
Sat, 06 Jul 2024 16:40:30 +0100

Download raw body.

Thread
Folks,

Here a reminder about this diff.

I'm using it for about two weeks and it jsut works.

The diff changes symantic of blacklist into:

     type list file [log]
             A file with domains to build the block or allow list. The block
             list allows querying all possible domains, and any matching
             domain returns a REFUSED response. The allow list allows querying
             only matching domains. With log blocked queries are logged. The
             list supports limited wildcard syntax: domains starting with .
             (dot) are treated as any subdomains on that zone.

So, after that this two lines:

google.com
*.google.com

blocks any requests to google.com and all its subdomains, or allows access
only to google.com which depends on type of this list.

The diff:

diff --git sbin/unwind/frontend.c sbin/unwind/frontend.c
index ccbc977eb73..6aa68aa0235 100644
--- sbin/unwind/frontend.c
+++ sbin/unwind/frontend.c
@@ -115,9 +115,11 @@ struct pending_query {
 
 TAILQ_HEAD(, pending_query)	 pending_queries;
 
-struct bl_node {
-	RB_ENTRY(bl_node)	 entry;
+struct l_node {
+	RB_ENTRY(l_node)	 entry;
 	char			*domain;
+	int			 len;
+	int			 wildcard;
 };
 
 __dead void		 frontend_shutdown(void);
@@ -148,9 +150,9 @@ struct pending_query	*find_pending_query(uint64_t);
 void			 parse_trust_anchor(struct trust_anchor_head *, int);
 void			 send_trust_anchors(struct trust_anchor_head *);
 void			 write_trust_anchors(struct trust_anchor_head *, int);
-void			 parse_blocklist(int);
-int			 bl_cmp(struct bl_node *, struct bl_node *);
-void			 free_bl(void);
+void			 parse_list(int);
+int			 l_cmp(struct l_node *, struct l_node *);
+void			 free_l(void);
 int			 pending_query_cnt(void);
 void			 check_available_af(void);
 
@@ -164,13 +166,25 @@ int			 ta_fd = -1;
 
 static struct trust_anchor_head	 trust_anchors, new_trust_anchors;
 
-RB_HEAD(bl_tree, bl_node)	 bl_head = RB_INITIALIZER(&bl_head);
-RB_PROTOTYPE(bl_tree, bl_node, entry, bl_cmp)
-RB_GENERATE(bl_tree, bl_node, entry, bl_cmp)
+RB_HEAD(l_tree, l_node)	 l_head = RB_INITIALIZER(&l_head);
+RB_PROTOTYPE(l_tree, l_node, entry, l_cmp)
+RB_GENERATE(l_tree, l_node, entry, l_cmp)
 
 struct dns64_prefix	*dns64_prefixes;
 int			 dns64_prefix_count;
 
+static void
+reverse(char* begin, char* end)
+{
+	char t;
+	while (begin < --end) {
+		t = *begin;
+		*begin = *end;
+		*end = t;
+		++begin;
+	}
+}
+
 void
 frontend_sig_handler(int sig, short event, void *bula)
 {
@@ -362,7 +376,7 @@ frontend_dispatch_main(int fd, short event, void *bula)
 			event_add(&iev_resolver->ev, NULL);
 			break;
 		case IMSG_RECONF_CONF:
-		case IMSG_RECONF_BLOCKLIST_FILE:
+		case IMSG_RECONF_LIST_FILE:
 		case IMSG_RECONF_FORWARDER:
 		case IMSG_RECONF_DOT_FORWARDER:
 		case IMSG_RECONF_FORCE:
@@ -373,8 +387,8 @@ frontend_dispatch_main(int fd, short event, void *bula)
 				fatalx("%s: IMSG_RECONF_END without "
 				    "IMSG_RECONF_CONF", __func__);
 			merge_config(frontend_conf, nconf);
-			if (frontend_conf->blocklist_file == NULL)
-				free_bl();
+			if (frontend_conf->list_file == NULL)
+				free_l();
 			nconf = NULL;
 			break;
 		case IMSG_UDP6SOCK:
@@ -454,11 +468,11 @@ frontend_dispatch_main(int fd, short event, void *bula)
 			if (!TAILQ_EMPTY(&trust_anchors))
 				send_trust_anchors(&trust_anchors);
 			break;
-		case IMSG_BLFD:
+		case IMSG_LSFD:
 			if ((fd = imsg_get_fd(&imsg)) == -1)
 				fatalx("%s: expected to receive imsg block "
 				   "list fd but didn't receive any", __func__);
-			parse_blocklist(fd);
+			parse_list(fd);
 			break;
 		default:
 			log_debug("%s: error handling imsg %d", __func__,
@@ -732,8 +746,8 @@ void
 handle_query(struct pending_query *pq)
 {
 	struct query_imsg	 query_imsg;
-	struct bl_node		 find;
-	int			 rcode;
+	struct l_node		 find;
+	int			 rcode, matched;
 	char			*str;
 	char			 dname[LDNS_MAX_DOMAINLEN + 1];
 	char			 qclass_buf[16];
@@ -790,9 +804,14 @@ handle_query(struct pending_query *pq)
 	log_debug("%s: %s %s %s ?", ip_port((struct sockaddr *)&pq->from),
 	    dname, qclass_buf, qtype_buf);
 
+	find.len = strlen(dname);
+	find.wildcard = 0;
+	reverse(dname, dname + find.len);
 	find.domain = dname;
-	if (RB_FIND(bl_tree, &bl_head, &find) != NULL) {
-		if (frontend_conf->blocklist_log)
+	matched = (RB_FIND(l_tree, &l_head, &find) != NULL);
+	reverse(dname, dname + find.len);
+	if (matched != frontend_conf->list_allowed) {
+		if (frontend_conf->list_log)
 			log_info("blocking %s", dname);
 		error_answer(pq, LDNS_RCODE_REFUSED);
 		goto send_answer;
@@ -1520,39 +1539,45 @@ out:
 }
 
 void
-parse_blocklist(int fd)
+parse_list(int fd)
 {
 	FILE		 *f;
-	struct bl_node	*bl_node;
+	struct l_node	 *l_node;
 	char		 *line = NULL;
 	size_t		  linesize = 0;
 	ssize_t		  linelen;
 
 	if((f = fdopen(fd, "r")) == NULL) {
-		log_warn("cannot read block list");
+		log_warn("cannot read list file");
 		close(fd);
 		return;
 	}
 
-	free_bl();
+	free_l();
 
 	while ((linelen = getline(&line, &linesize, f)) != -1) {
 		if (line[linelen - 1] == '\n') {
 			if (linelen >= 2 && line[linelen - 2] != '.')
 				line[linelen - 1] = '.';
 			else
-				line[linelen - 1] = '\0';
+				line[linelen-- - 1] = '\0';
 		}
 
-		bl_node = malloc(sizeof *bl_node);
-		if (bl_node == NULL)
+		if (line[0] == '#')
+		    continue;
+
+		l_node = malloc(sizeof *l_node);
+		if (l_node == NULL)
 			fatal("%s: malloc", __func__);
-		if ((bl_node->domain = strdup(line)) == NULL)
+		if ((l_node->domain = strdup(line)) == NULL)
 			fatal("%s: strdup", __func__);
-		if (RB_INSERT(bl_tree, &bl_head, bl_node) != NULL) {
-			log_warnx("duplicate blocked domain \"%s\"", line);
-			free(bl_node->domain);
-			free(bl_node);
+		reverse(l_node->domain, l_node->domain + linelen);
+		l_node->len = linelen;
+		l_node->wildcard = line[0] == '.';
+		if (RB_INSERT(l_tree, &l_head, l_node) != NULL) {
+			log_warnx("duplicate list domain \"%s\"", line);
+			free(l_node->domain);
+			free(l_node);
 		}
 	}
 	free(line);
@@ -1562,17 +1587,22 @@ parse_blocklist(int fd)
 }
 
 int
-bl_cmp(struct bl_node *e1, struct bl_node *e2) {
-	return (strcasecmp(e1->domain, e2->domain));
+l_cmp(struct l_node *e1, struct l_node *e2) {
+	if (e1->wildcard == e2->wildcard)
+		return (strcasecmp(e1->domain, e2->domain));
+	else if (e1->wildcard)
+		return (strncasecmp(e1->domain, e2->domain, e1->len));
+	else /* e2->wildcard */
+		return (strncasecmp(e1->domain, e2->domain, e2->len));
 }
 
 void
-free_bl(void)
+free_l(void)
 {
-	struct bl_node	*n, *nxt;
+	struct l_node	*n, *nxt;
 
-	RB_FOREACH_SAFE(n, bl_tree, &bl_head, nxt) {
-		RB_REMOVE(bl_tree, &bl_head, n);
+	RB_FOREACH_SAFE(n, l_tree, &l_head, nxt) {
+		RB_REMOVE(l_tree, &l_head, n);
 		free(n->domain);
 		free(n);
 	}
diff --git sbin/unwind/parse.y sbin/unwind/parse.y
index 79231e3d5be..271a325656d 100644
--- sbin/unwind/parse.y
+++ sbin/unwind/parse.y
@@ -102,11 +102,11 @@ typedef struct {
 %token	INCLUDE ERROR
 %token	FORWARDER DOT PORT ODOT_FORWARDER ODOT_AUTOCONF ODOT_DHCP
 %token	AUTHENTICATION NAME PREFERENCE RECURSOR AUTOCONF DHCP STUB
-%token	BLOCK LIST LOG FORCE ACCEPT BOGUS
+%token	ALLOW BLOCK LIST LOG FORCE ACCEPT BOGUS
 
 %token	<v.string>	STRING
 %token	<v.number>	NUMBER
-%type	<v.number>	port dot prefopt log acceptbogus
+%type	<v.number>	port dot prefopt log acceptbogus list_allowed
 %type	<v.string>	string authname
 %type	<v.force>	force_list
 
@@ -118,7 +118,7 @@ grammar		: /* empty */
 		| grammar varset '\n'
 		| grammar uw_pref '\n'
 		| grammar uw_forwarder '\n'
-		| grammar block_list '\n'
+		| grammar the_list '\n'
 		| grammar force '\n'
 		| grammar error '\n'		{ file->errors++; }
 		;
@@ -176,22 +176,28 @@ optnl		: '\n' optnl		/* zero or more newlines */
 		| /*empty*/
 		;
 
-block_list		: BLOCK LIST STRING log {
-				if (conf->blocklist_file != NULL) {
-					yyerror("block list already "
-					    "configured");
+the_list		: list_allowed LIST STRING log {
+				if (conf->list_file != NULL) {
+					yyerror("the %s list already "
+					    "configured",
+					    $1 ? "allow" : "block");
 					free($3);
 					YYERROR;
 				} else {
-					conf->blocklist_file = strdup($3);
-					if (conf->blocklist_file == NULL)
+					conf->list_file = strdup($3);
+					if (conf->list_file == NULL)
 						err(1, "strdup");
 					free($3);
-					conf->blocklist_log = $4;
+					conf->list_log = $4;
+					conf->list_allowed = $1;
 				}
 			}
 			;
 
+list_allowed:	BLOCK	{ $$ = 0; }
+	|	ALLOW	{ $$ = 1; }
+	;
+
 uw_pref			: PREFERENCE {
 				conf->res_pref.len = 0;
 				memset(conf->enabled_resolvers, 0,
@@ -417,6 +423,7 @@ lookup(char *s)
 	static const struct keywords keywords[] = {
 		{"DoT",			DOT},
 		{"accept",		ACCEPT},
+		{"allow",		ALLOW},
 		{"authentication",	AUTHENTICATION},
 		{"autoconf",		AUTOCONF},
 		{"block",		BLOCK},
diff --git sbin/unwind/printconf.c sbin/unwind/printconf.c
index c2ee982c442..58fbaf2132a 100644
--- sbin/unwind/printconf.c
+++ sbin/unwind/printconf.c
@@ -66,9 +66,11 @@ print_config(struct uw_conf *conf)
 		printf("}\n");
 	}
 
-	if (conf->blocklist_file != NULL)
-		printf("block list \"%s\"%s\n", conf->blocklist_file,
-		    conf->blocklist_log ? " log" : "");
+	if (conf->list_file != NULL)
+		printf("%s list \"%s\"%s\n",
+		    conf->list_allowed ? "allow" : "block",
+		    conf->list_file,
+		    conf->list_log ? " log" : "");
 	for (j = 0; j < UW_RES_NONE; j++) {
 		struct force_tree_entry	*e;
 		int			 empty = 1;
diff --git sbin/unwind/resolver.c sbin/unwind/resolver.c
index ee2a83cbe2a..17fc0d43976 100644
--- sbin/unwind/resolver.c
+++ sbin/unwind/resolver.c
@@ -682,7 +682,7 @@ resolver_dispatch_main(int fd, short event, void *bula)
 				fatal("pledge");
 			break;
 		case IMSG_RECONF_CONF:
-		case IMSG_RECONF_BLOCKLIST_FILE:
+		case IMSG_RECONF_LIST_FILE:
 		case IMSG_RECONF_FORWARDER:
 		case IMSG_RECONF_DOT_FORWARDER:
 		case IMSG_RECONF_FORCE:
diff --git sbin/unwind/unwind.c sbin/unwind/unwind.c
index 1b70b6e3e6c..df1311c8417 100644
--- sbin/unwind/unwind.c
+++ sbin/unwind/unwind.c
@@ -73,7 +73,7 @@ int		main_reload(void);
 int		main_sendall(enum imsg_type, void *, uint16_t);
 void		open_ports(void);
 void		solicit_dns_proposals(void);
-void		send_blocklist_fd(void);
+void		send_list_fd(void);
 
 struct uw_conf		*main_conf;
 static struct imsgev	*iev_frontend;
@@ -290,8 +290,8 @@ main(int argc, char *argv[])
 	main_imsg_compose_frontend_fd(IMSG_ROUTESOCK, 0, frontend_routesock);
 	main_imsg_send_config(main_conf);
 
-	if (main_conf->blocklist_file != NULL)
-		send_blocklist_fd();
+	if (main_conf->list_file != NULL)
+		send_list_fd();
 
 	if (pledge("stdio rpath sendfd", NULL) == -1)
 		fatal("pledge");
@@ -577,8 +577,8 @@ main_reload(void)
 
 	merge_config(main_conf, xconf);
 
-	if (main_conf->blocklist_file != NULL)
-		send_blocklist_fd();
+	if (main_conf->list_file != NULL)
+		send_list_fd();
 
 	return (0);
 }
@@ -593,10 +593,10 @@ main_imsg_send_config(struct uw_conf *xconf)
 	if (main_sendall(IMSG_RECONF_CONF, xconf, sizeof(*xconf)) == -1)
 		return (-1);
 
-	if (xconf->blocklist_file != NULL) {
-		if (main_sendall(IMSG_RECONF_BLOCKLIST_FILE,
-		    xconf->blocklist_file, strlen(xconf->blocklist_file) + 1)
-		    == -1)
+	if (xconf->list_file != NULL) {
+		if (main_sendall(IMSG_RECONF_LIST_FILE,
+		    xconf->list_file, strlen(xconf->list_file) + 1)
+		== -1)
 			return (-1);
 	}
 
@@ -667,9 +667,10 @@ merge_config(struct uw_conf *conf, struct uw_conf *xconf)
 	memcpy(&conf->res_pref, &xconf->res_pref,
 	    sizeof(conf->res_pref));
 
-	free(conf->blocklist_file);
-	conf->blocklist_file = xconf->blocklist_file;
-	conf->blocklist_log = xconf->blocklist_log;
+	free(conf->list_file);
+	conf->list_file = xconf->list_file;
+	conf->list_log = xconf->list_log;
+	conf->list_allowed = xconf->list_allowed;
 
 	/* Add new forwarders. */
 	TAILQ_CONCAT(&conf->uw_forwarder_list, &xconf->uw_forwarder_list,
@@ -861,14 +862,14 @@ solicit_dns_proposals(void)
 }
 
 void
-send_blocklist_fd(void)
+send_list_fd(void)
 {
-	int	bl_fd;
+	int	fd;
 
-	if ((bl_fd = open(main_conf->blocklist_file, O_RDONLY)) != -1)
-		main_imsg_compose_frontend_fd(IMSG_BLFD, 0, bl_fd);
+	if ((fd = open(main_conf->list_file, O_RDONLY)) != -1)
+		main_imsg_compose_frontend_fd(IMSG_LSFD, 0, fd);
 	else
-		log_warn("%s", main_conf->blocklist_file);
+		log_warn("%s", main_conf->list_file);
 }
 
 void
@@ -896,11 +897,10 @@ imsg_receive_config(struct imsg *imsg, struct uw_conf **xconf)
 		TAILQ_INIT(&nconf->uw_dot_forwarder_list);
 		RB_INIT(&nconf->force);
 		break;
-	case IMSG_RECONF_BLOCKLIST_FILE:
+	case IMSG_RECONF_LIST_FILE:
 		if (((char *)imsg->data)[IMSG_DATA_SIZE(*imsg) - 1] != '\0')
-			fatalx("Invalid blocklist file");
-		if ((nconf->blocklist_file = strdup(imsg->data)) ==
-		    NULL)
+			fatalx("Invalid allowlist file");
+		if ((nconf->list_file = strdup(imsg->data)) == NULL)
 			fatal("%s: strdup", __func__);
 		break;
 	case IMSG_RECONF_FORWARDER:
diff --git sbin/unwind/unwind.conf.5 sbin/unwind/unwind.conf.5
index dbcec1bd693..7344c2f81ac 100644
--- sbin/unwind/unwind.conf.5
+++ sbin/unwind/unwind.conf.5
@@ -63,15 +63,25 @@ forwarder { $fwd1 $fwd2 }
 .Ed
 .Sh GLOBAL CONFIGURATION
 .Bl -tag -width Ds
-.It Ic block list Ar file Op Cm log
-A file containing domains to block, one per line.
-If a domain from this list is queried,
-.Nm unwind
-answers with a return code of
-.Dv REFUSED .
-With
+.It Ar type Ic list Ar file Op Cm log
+A
+.Ar file
+with domains to build the
+.Ic block
+or
+.Ic allow
+list. The
+.Ic block
+list allows querying all possible domains, and any matching
+domain returns a
+.Dv REFUSED
+response. The
+.Ic allow
+list allows querying only matching domains. With
 .Cm log
-blocked queries are logged.
+blocked queries are logged. The list supports limited wildcard
+syntax: domains starting with . (dot) are treated as any subdomains
+on that zone.
 .It Ic forwarder Brq Ar address Oo Ic port Ar number Oc Oo Oo Ic authentication name Ar name Oc Ic DoT Oc ...
 A list of addresses of DNS name servers to forward queries to.
 .Ic port
@@ -168,6 +178,20 @@ to use a specific resolver type:
 .Bd -literal -offset indent
 force autoconf { domain.local }
 .Ed
+.Pp
+Allow requests for domains in openbsd.org zone and log each
+blocked request:
+.Bd -literal -offset indent
+allow list "/etc/allowlist" log
+.Ed
+.Pp
+Where a list of allowed doamins
+.Pa /etc/allowlist :
+.Bd -literal -offset indent
+openbsd.org
+.openbsd.org
+.Ed
+.Pp
 .Sh SEE ALSO
 .Xr rc.conf.local 8 ,
 .Xr unwind 8 ,
diff --git sbin/unwind/unwind.h sbin/unwind/unwind.h
index f21baf72970..2847fdf389f 100644
--- sbin/unwind/unwind.h
+++ sbin/unwind/unwind.h
@@ -90,7 +90,7 @@ enum imsg_type {
 	IMSG_CTL_AUTOCONF,
 	IMSG_CTL_MEM,
 	IMSG_RECONF_CONF,
-	IMSG_RECONF_BLOCKLIST_FILE,
+	IMSG_RECONF_LIST_FILE,
 	IMSG_RECONF_FORWARDER,
 	IMSG_RECONF_DOT_FORWARDER,
 	IMSG_RECONF_FORCE,
@@ -117,7 +117,7 @@ enum imsg_type {
 	IMSG_NEW_TAS_ABORT,
 	IMSG_NEW_TAS_DONE,
 	IMSG_NETWORK_CHANGED,
-	IMSG_BLFD,
+	IMSG_LSFD,
 	IMSG_REPLACE_DNS,
 	IMSG_NEW_DNS64_PREFIXES_START,
 	IMSG_NEW_DNS64_PREFIX,
@@ -155,8 +155,9 @@ struct uw_conf {
 	struct force_tree		 force;
 	struct resolver_preference	 res_pref;
 	int				 enabled_resolvers[UW_RES_NONE];
-	char				*blocklist_file;
-	int				 blocklist_log;
+	char				*list_file;
+	int				 list_log;
+	int				 list_allowed;
 };
 
 struct query_imsg {

-- 
wbr, Kirill