Hello,

There was a post several days ago about AAAA filter, and questions about an implementation as a Python module by Christophe.

https://unbound.net/pipermail/unbound-users/2014-October/003579.html

I work at the same company as him (a Japanese ISP, which are all subjected to NTT's flaky practices with use of IPv6), and have been working with him on this issue.

To sum up the scenario we are trying to fix :
- customers in Japan have a physical carrier (NTT in most cases) on top of which they get their own internet provider, to which they connect via PPPoE. In our case, we currently provide only IPv4 service at this point in time. - some customers also get an on-demand video service via his carrier, NTT, which give them a default IPv6 route via IPoE, or they raise a second PPPoE session (NTT usage terms allow for up to 2 sessions for this scenario). - the catch: said IPv6 default route does not go outside on the internet, and only enables access to NTT's closed network. Therefore, a customer in this configuration trying to access any IPv6 site is in for a world of pain as his browser times out and retries, hopefully fallbacking to IPv4. This prompted every Internet service provider in Japan to either provide native IPv6 or to filter AAAA records for "non-v6 only sites", which in this instance pretty much means everyone uses BIND.


To answer Bill Manning's earlier statement, "we can not change providers", first reason being because we ARE a provider, but also because even if we wanted to change carriers, everyone in Japan is entirely dependant on the majority physical carrier, i.e NTT. Since it happens at a lower layer than the ones we have control over (we work at PPPoE encapsulated level, they work at Ethernet level), we have no control whatsoever over said carrier-provided route, short of ourselves providing IPv6 service over or PPPoE and an overriding route, or via IPoE (and then tell NTT to buzz off and provide our route, if we had one). This is obviously scheduled as the proper solution, but requires overhauling all of the network infrastructure, which can not be done instantly.

Also, thanks a lot to Daisuke Higashi for his statement, using "private-address/private-domain" is initially what we planned on doing, except this gets scary when we think about "what if NTT springs yet another domain on top of that, that we need to allow access to?" or "what if another customer tries to access yet another IPv6-only site in the future?", and the "whack-a-mole" administration nightmare it might become.



In the meantime, we still need to get rid of BIND, which can't handle the resource exhaustion DDoS attacks (http://dnsamplificationattacks.blogspot.jp/2014/02/authoritative-name-server-attack.html) we are seeing since february of this year. This is where we wanted to go with unbound, except since it does not have AAAA-filter functionality, we could not use it in production for most of our customers.

This is where Christophe attempted to intercept queries with Python and we found out : - Python API does not enable to spawn sub queries (for each AAAA query, the relevant A record has to exist in cache, for AAAA filter logic to work) - Python API does not enable to lookup RRset cache for a given record (in this case, for an A record matching the queried name) - Python API does not allow for easy scrubbing of packets (it IS possible, but very painful)



I therefore came to the conclusion that the Python API was not appropriate to do these things, and that the most appropriate place to implement a filtering/scrubbing logic was the iterator module itself.

I coded the following patch (also attached) : http://www.yomi.darkbsd.org/~darksoul/aaaa-filter-iterator.patch It has been on tests and running in pre-production for roughly a month now (while undergoing some tuning as I got around to understanding how the state machine works).

The patch provides :
- a "aaaa-filter" config option which is off by default, so as to not be intrusive (I am fully aware this functionality is enough of an abomination as is). It can also be used in conjunction to private-address/private-domain without any issues.
- the relevant manual entry
- modifications to iterator/iter_scrub.c in scrub_sanitize() to remove AAAA records for queries that either are not AAAA type, OR that did return an A record, IF cfg->aaaa_filter is enabled. - modifications to iterator/iter_utils.c to provide AAAA filter "on/off" info to the iterator environment.
- modifications to iterator/iterator.c :
-- a new ASN_FETCH_A_FOR_AAAA_STATE from which we branch into from QUERYTARGETS_STATE if this is a AAAA query (modifies iter_handle(), iter_state_to_string(), iter_state_is_responsestate(), -- asn_processQueryAAAA() function that throws a subquery and flags the parent query as "already fetching an A subquery" so as to not loop -- modifications to iter_inform_super() to handle the new state for AAAA parent queries having a A subquery. -- asn_processAAAAResponse() function that basically takes after what error_supers() and processTargetResponse() do, except it does not alter target queries counters. - modifications to iterator/iterator.h : declaration of new flags for iter_env (configuration option), iter_qstate (status flag), and the new iter_state



At this point, the patch is pretty much stable and performing as expected, but I am looking for pointers as to stuff I could improve on that patch, especially style-wise, to ensure it is applicable as long as possible. In its current state, I can apply it up to 1.4.22.

I also know from previous postings that unbound development staff's opinion is that this functionality as a whole would harm IPv6 adoption, and therefore can probably not be officially endorsed, but I still intend to provide it freely (my company has given approval) to people suffering from the same scenario. (that is, mainly Japanese users at this point...)

Thanks for your time,
--
Stephane LAPIE, EPITA SRS, Promo 2005
"Even when they have digital readouts, I can't understand them."
--MegaTokyo
--- unbound-1.4.17.orig/doc/unbound.conf.5.in
+++ unbound-1.4.17/doc/unbound.conf.5.in
@@ -519,6 +519,13 @@ authority servers and checks if the repl
 Disabled by default. 
 This feature is an experimental implementation of draft dns\-0x20.
 .TP
+.B aaaa\-filter: \fI<yes or no>
+Activate behavior similar to BIND's AAAA-filter.
+This forces the dropping of all AAAA records, unless in the case of
+explicit AAAA queries, when no A records have been confirmed.
+This also causes an additional A query to be sent for each AAAA query.
+This breaks DNSSEC!
+.TP
 .B private\-address: \fI<IP address or subnet>
 Give IPv4 of IPv6 addresses or classless subnets. These are addresses
 on your private network, and are not allowed to be returned for public
--- unbound-1.4.17.orig/util/config_file.c
+++ unbound-1.4.17/util/config_file.c
@@ -160,6 +160,7 @@ config_create(void)
 	cfg->harden_below_nxdomain = 0;
 	cfg->harden_referral_path = 0;
 	cfg->use_caps_bits_for_id = 0;
+	cfg->aaaa_filter = 0; /* ASN: default is disabled */
 	cfg->private_address = NULL;
 	cfg->private_domain = NULL;
 	cfg->unwanted_threshold = 0;
--- unbound-1.4.17.orig/iterator/iter_scrub.c
+++ unbound-1.4.17/iterator/iter_scrub.c
@@ -580,6 +580,32 @@ static int sanitize_nsec_is_overreach(st
 }
 
 /**
+ * ASN: Lookup A records from rrset cache.
+ * @param qinfo: the question originally asked.
+ * @param env: module environment with config and cache.
+ * @param ie: iterator environment with private address data.
+ * @return 0 if no A record found, 1 if A record found.
+ */
+static int
+asn_lookup_a_record_from_cache(struct query_info* qinfo,
+	struct module_env* env, struct iter_env* ie)
+{
+	struct ub_packed_rrset_key* akey;
+
+	/* get cached A records for queried name */
+	akey = rrset_cache_lookup(env->rrset_cache, qinfo->qname,
+		qinfo->qname_len, LDNS_RR_TYPE_A, qinfo->qclass,
+		0, *env->now, 0);
+	if(akey) { /* we had some. */
+		log_rrset_key(VERB_ALGO, "ASN-AAAA-filter: found A record",
+			      akey);
+		lock_rw_unlock(&akey->entry.lock);
+		return 1;
+	}
+	return 0;
+}
+
+/**
  * Given a response event, remove suspect RRsets from the response.
  * "Suspect" rrsets are potentially poison. Note that this routine expects
  * the response to be in a "normalized" state -- that is, all "irrelevant"
@@ -598,6 +625,7 @@ scrub_sanitize(ldns_buffer* pkt, struct
 	struct query_info* qinfo, uint8_t* zonename, struct module_env* env,
 	struct iter_env* ie)
 {
+	int found_a_record = 0; /* ASN: do we have a A record? */
 	int del_addi = 0; /* if additional-holding rrsets are deleted, we
 		do not trust the normalized additional-A-AAAA any more */
 	struct rrset_parse* rrset, *prev;
@@ -633,6 +661,13 @@ scrub_sanitize(ldns_buffer* pkt, struct
 		rrset = rrset->rrset_all_next;
 	}
 
+	/* ASN: Locate any A record we can find */
+	if((ie->aaaa_filter) && (qinfo->qtype == LDNS_RR_TYPE_AAAA)) {
+		found_a_record = asn_lookup_a_record_from_cache(qinfo,
+			env, ie);
+	}
+	/* ASN: End of added code */
+
 	/* At this point, we brutally remove ALL rrsets that aren't 
 	 * children of the originating zone. The idea here is that, 
 	 * as far as we know, the server that we contacted is ONLY 
@@ -644,6 +679,24 @@ scrub_sanitize(ldns_buffer* pkt, struct
 	rrset = msg->rrset_first;
 	while(rrset) {
 
+		/* ASN: For AAAA records only... */
+		if((ie->aaaa_filter) && (rrset->type == LDNS_RR_TYPE_AAAA)) {
+			/* ASN: If this is not a AAAA query, then remove AAAA
+			 * records, no questions asked. If this IS a AAAA query
+			 * then remove AAAA records if we have an A record.
+			 * Otherwise, leave things be. */
+			if((qinfo->qtype != LDNS_RR_TYPE_AAAA) ||
+				(found_a_record)) {
+				remove_rrset("ASN-AAAA-filter: removing AAAA "
+					"for record", pkt, msg, prev, &rrset);
+				continue;
+			}
+			log_nametypeclass(VERB_ALGO, "ASN-AAAA-filter: "
+				"keep AAAA for", zonename,
+				LDNS_RR_TYPE_AAAA, qinfo->qclass);
+		}
+		/* ASN: End of added code */
+
 		/* remove private addresses */
 		if( (rrset->type == LDNS_RR_TYPE_A || 
 			rrset->type == LDNS_RR_TYPE_AAAA) &&
--- unbound-1.4.17.orig/iterator/iterator.c
+++ unbound-1.4.17/iterator/iterator.c
@@ -1579,6 +1579,53 @@ processDSNSFind(struct module_qstate* qs
 
 	return 0;
 }
+
+/**
+ * ASN: This event state was added as an intermediary step between
+ * QUERYTARGETS_STATE and the next step, in order to cast a subquery for the
+ * purpose of caching A records for the queried name.
+ * 
+ * @param qstate: query state.
+ * @param iq: iterator query state.
+ * @param ie: iterator shared global environment.
+ * @param id: module id.
+ * @return true if the event requires more request processing immediately,
+ *         false if not. This state only returns true when it is generating
+ *         a SERVFAIL response because the query has hit a dead end.
+ */
+static int
+asn_processQueryAAAA(struct module_qstate* qstate, struct iter_qstate* iq,
+	struct iter_env* ie, int id)
+{
+	struct module_qstate* subq = NULL;
+
+	log_assert(iq->fetch_a_for_aaaa == 0);
+
+	/* flag the query properly in order to not loop */
+	iq->fetch_a_for_aaaa = 1;
+
+	/* re-throw same query, but with a different type */
+	if(!generate_sub_request(iq->qchase.qname,
+        	iq->qchase.qname_len, LDNS_RR_TYPE_A,
+		iq->qchase.qclass, qstate, id, iq,
+		INIT_REQUEST_STATE, FINISHED_STATE, &subq, 1)) {
+		log_nametypeclass(VERB_ALGO, "ASN-AAAA-filter: failed "
+			"preloading of A record for",
+			iq->qchase.qname, LDNS_RR_TYPE_A,
+			iq->qchase.qclass);
+		return error_response(qstate, id, LDNS_RCODE_SERVFAIL);
+	}
+	log_nametypeclass(VERB_ALGO, "ASN-AAAA-filter: "
+		"preloading records in cache for",
+		iq->qchase.qname, LDNS_RR_TYPE_A,
+		iq->qchase.qclass);
+
+	/* set this query as waiting */
+	qstate->ext_state[id] = module_wait_subquery;
+	/* at this point break loop */
+	return 0;
+}
+/* ASN: End of added code */
 	
 /** 
  * This is the request event state where the request will be sent to one of
@@ -1626,6 +1673,13 @@ processQueryTargets(struct module_qstate
 		return error_response(qstate, id, LDNS_RCODE_SERVFAIL);
 	}
 	
+	/* ASN: If we have a AAAA query, then also query for A records */
+	if((ie->aaaa_filter) && (iq->qchase.qtype == LDNS_RR_TYPE_AAAA) &&
+		(iq->fetch_a_for_aaaa == 0)) {
+		return next_state(iq, ASN_FETCH_A_FOR_AAAA_STATE);
+	}
+	/* ASN: End of added code */
+
 	/* Make sure we have a delegation point, otherwise priming failed
 	 * or another failure occurred */
 	if(!iq->dp) {
@@ -2568,6 +2622,62 @@ processFinished(struct module_qstate* qs
 	return 0;
 }
 
+/** 
+ * ASN: Do final processing on responses to A queries originated from AAAA
+ * queries. Events reach this state after the iterative resolution algorithm
+ * terminates.
+ * This is required down the road to decide whether to scrub AAAA records
+ * from the results or not.
+ *
+ * @param qstate: query state.
+ * @param id: module id.
+ * @param forq: super query state.
+ */
+static void
+asn_processAAAAResponse(struct module_qstate* qstate, int id,
+	struct module_qstate* super)
+{
+	struct iter_qstate* iq = (struct iter_qstate*)qstate->minfo[id];
+	struct iter_qstate* super_iq = (struct iter_qstate*)super->minfo[id];
+	struct ub_packed_rrset_key* rrset;
+	struct delegpt_ns* dpns = NULL;
+	int error = (qstate->return_rcode != LDNS_RCODE_NOERROR);
+
+	log_assert(super_iq->fetch_a_for_aaaa > 0);
+
+	/* let super go to evaluation of targets after this */
+	super_iq->state = QUERYTARGETS_STATE;
+
+	log_query_info(VERB_ALGO, "ASN-AAAA-filter: processAAAAResponse",
+		&qstate->qinfo);
+	log_query_info(VERB_ALGO, "ASN-AAAA-filter: processAAAAResponse super",
+		&super->qinfo);
+
+	if(super_iq->dp)
+		dpns = delegpt_find_ns(super_iq->dp,
+			qstate->qinfo.qname, qstate->qinfo.qname_len);
+	if (!dpns) {
+		/* not interested */
+		verbose(VERB_ALGO, "ASN-AAAA-filter: subq: %s, but parent not "
+			"interested%s", (error ? "error, but" : "success"),
+			(super_iq->dp ? "anymore" : " (was reset)"));
+		log_query_info(VERB_ALGO, "ASN-AAAA-filter: superq", &super->qinfo);
+		if(super_iq->dp && error)
+			delegpt_log(VERB_ALGO, super_iq->dp);
+		return;
+	} else if (error) {
+		verbose(VERB_ALGO, "ASN-AAAA-filter: mark as failed, "
+			"and go to target query.");
+		/* see if the failure did get (parent-lame) info */
+		if(!cache_fill_missing(super->env,
+			super_iq->qchase.qclass, super->region,
+			super_iq->dp))
+		log_err("ASN-AAAA-filter: out of memory adding missing");
+		dpns->resolved = 1; /* mark as failed */
+	}
+}
+/* ASN: End of added code */
+
 /*
  * Return priming query results to interestes super querystates.
  * 
@@ -2587,6 +2697,9 @@ iter_inform_super(struct module_qstate*
 	else if(super->qinfo.qtype == LDNS_RR_TYPE_DS && ((struct iter_qstate*)
 		super->minfo[id])->state == DSNS_FIND_STATE)
 		processDSNSResponse(qstate, id, super);
+	else if (super->qinfo.qtype == LDNS_RR_TYPE_AAAA && ((struct iter_qstate*)
+		super->minfo[id])->state == ASN_FETCH_A_FOR_AAAA_STATE)
+		asn_processAAAAResponse(qstate, id, super);
 	else if(qstate->return_rcode != LDNS_RCODE_NOERROR)
 		error_supers(qstate, id, super);
 	else if(qstate->is_priming)
@@ -2624,6 +2737,9 @@ iter_handle(struct module_qstate* qstate
 			case INIT_REQUEST_3_STATE:
 				cont = processInitRequest3(qstate, iq, id);
 				break;
+			case ASN_FETCH_A_FOR_AAAA_STATE:
+				cont = asn_processQueryAAAA(qstate, iq, ie, id);
+				break;
 			case QUERYTARGETS_STATE:
 				cont = processQueryTargets(qstate, iq, ie, id);
 				break;
@@ -2863,6 +2979,8 @@ iter_state_to_string(enum iter_state sta
 		return "INIT REQUEST STATE (stage 2)";
 	case INIT_REQUEST_3_STATE:
 		return "INIT REQUEST STATE (stage 3)";
+	case ASN_FETCH_A_FOR_AAAA_STATE:
+		return "ASN_FETCH_A_FOR_AAAA_STATE";
 	case QUERYTARGETS_STATE :
 		return "QUERY TARGETS STATE";
 	case PRIME_RESP_STATE :
@@ -2887,6 +3005,7 @@ iter_state_is_responsestate(enum iter_st
 		case INIT_REQUEST_STATE :
 		case INIT_REQUEST_2_STATE :
 		case INIT_REQUEST_3_STATE :
+		case ASN_FETCH_A_FOR_AAAA_STATE :
 		case QUERYTARGETS_STATE :
 		case COLLECT_CLASS_STATE :
 			return 0;
--- unbound-1.4.17.orig/iterator/iter_utils.c
+++ unbound-1.4.17/iterator/iter_utils.c
@@ -128,6 +128,7 @@ iter_apply_cfg(struct iter_env* iter_env
 	}
 	iter_env->supports_ipv6 = cfg->do_ip6;
 	iter_env->supports_ipv4 = cfg->do_ip4;
+	iter_env->aaaa_filter = cfg->aaaa_filter;
 	return 1;
 }
 
--- unbound-1.4.17.orig/iterator/iterator.h
+++ unbound-1.4.17/iterator/iterator.h
@@ -110,6 +110,9 @@ struct iter_env {
 	 * array of max_dependency_depth+1 size.
 	 */
 	int* target_fetch_policy;
+
+	/** ASN: AAAA-filter flag */
+	int aaaa_filter;
 };
 
 /**
@@ -135,6 +138,14 @@ enum iter_state {
 	INIT_REQUEST_3_STATE,
 
 	/**
+	 * This state is responsible for intercepting AAAA queries,
+	 * and launch a A subquery on the same target, to populate the
+	 * cache with A records, so the AAAA filter scrubbing logic can
+	 * work.
+	 */
+	ASN_FETCH_A_FOR_AAAA_STATE,
+
+	/**
 	 * Each time a delegation point changes for a given query or a 
 	 * query times out and/or wakes up, this state is (re)visited. 
 	 * This state is reponsible for iterating through a list of 
@@ -309,6 +320,13 @@ struct iter_qstate {
 	 */
 	int refetch_glue;
 
+	/**
+	 * ASN: This is a flag that, if true, means that this query is
+	 * for fetching A records to populate cache and determine if we must
+	 * return AAAA records or not.
+	 */
+	int fetch_a_for_aaaa;
+
 	/** list of pending queries to authoritative servers. */
 	struct outbound_list outlist;
 };
--- unbound-1.4.17.orig/util/config_file.h
+++ unbound-1.4.17/util/config_file.h
@@ -169,6 +169,8 @@ struct config_file {
 	int harden_referral_path;
 	/** use 0x20 bits in query as random ID bits */
 	int use_caps_bits_for_id;
+	/** ASN: enable AAAA filter? */
+	int aaaa_filter;
 	/** strip away these private addrs from answers, no DNS Rebinding */
 	struct config_strlist* private_address;
 	/** allow domain (and subdomains) to use private address space */
--- unbound-1.4.17.orig/util/configlexer.lex
+++ unbound-1.4.17/util/configlexer.lex
@@ -177,6 +177,7 @@ harden-below-nxdomain{COLON}	{ YDVAR(1,
 harden-referral-path{COLON}	{ YDVAR(1, VAR_HARDEN_REFERRAL_PATH) }
 use-caps-for-id{COLON}		{ YDVAR(1, VAR_USE_CAPS_FOR_ID) }
 unwanted-reply-threshold{COLON}	{ YDVAR(1, VAR_UNWANTED_REPLY_THRESHOLD) }
+aaaa-filter{COLON}		{ YDVAR(1, VAR_AAAA_FILTER) }
 private-address{COLON}		{ YDVAR(1, VAR_PRIVATE_ADDRESS) }
 private-domain{COLON}		{ YDVAR(1, VAR_PRIVATE_DOMAIN) }
 prefetch-key{COLON}		{ YDVAR(1, VAR_PREFETCH_KEY) }
--- unbound-1.4.17.orig/util/configparser.y
+++ unbound-1.4.17/util/configparser.y
@@ -92,6 +92,7 @@ extern struct config_parser_state* cfg_p
 %token VAR_STATISTICS_CUMULATIVE VAR_OUTGOING_PORT_PERMIT 
 %token VAR_OUTGOING_PORT_AVOID VAR_DLV_ANCHOR_FILE VAR_DLV_ANCHOR
 %token VAR_NEG_CACHE_SIZE VAR_HARDEN_REFERRAL_PATH VAR_PRIVATE_ADDRESS
+%token VAR_AAAA_FILTER
 %token VAR_PRIVATE_DOMAIN VAR_REMOTE_CONTROL VAR_CONTROL_ENABLE
 %token VAR_CONTROL_INTERFACE VAR_CONTROL_PORT VAR_SERVER_KEY_FILE
 %token VAR_SERVER_CERT_FILE VAR_CONTROL_KEY_FILE VAR_CONTROL_CERT_FILE
@@ -151,6 +152,7 @@ content_server: server_num_threads | ser
 	server_dlv_anchor_file | server_dlv_anchor | server_neg_cache_size |
 	server_harden_referral_path | server_private_address |
 	server_private_domain | server_extended_statistics | 
+	server_aaaa_filter |
 	server_local_data_ptr | server_jostle_timeout | 
 	server_unwanted_reply_threshold | server_log_time_ascii | 
 	server_domain_insecure | server_val_sig_skew_min | 
@@ -802,6 +803,15 @@ server_use_caps_for_id: VAR_USE_CAPS_FOR
 		free($2);
 	}
 	;
+server_aaaa_filter: VAR_AAAA_FILTER STRING_ARG
+	{
+		OUTYY(("P(server_aaaa_filter:%s)\n", $2));
+		if(strcmp($2, "yes") != 0 && strcmp($2, "no") != 0)
+			yyerror("expected yes or no.");
+		else cfg_parser->cfg->aaaa_filter = (strcmp($2, "yes")==0);
+		free($2);
+	}
+	;
 server_private_address: VAR_PRIVATE_ADDRESS STRING_ARG
 	{
 		OUTYY(("P(server_private_address:%s)\n", $2));
--- unbound-1.4.17.orig/pythonmod/interface.i
+++ unbound-1.4.17/pythonmod/interface.i
@@ -626,6 +626,7 @@ struct config_file {
    int harden_dnssec_stripped;
    int harden_referral_path;
    int use_caps_bits_for_id;
+   int aaaa_filter; /* ASN */
    struct config_strlist* private_address;
    struct config_strlist* private_domain;
    size_t unwanted_threshold;
_______________________________________________
Unbound-users mailing list
[email protected]
http://unbound.nlnetlabs.nl/mailman/listinfo/unbound-users

Reply via email to