diff --git a/src/libknot/nameserver/name-server.c b/src/libknot/nameserver/name-server.c
index ac2c0c355bac0df3ef1161581e3b300fee22512f..068dac7d3f4a5230e0225d73035cf4c2b7429432 100644
--- a/src/libknot/nameserver/name-server.c
+++ b/src/libknot/nameserver/name-server.c
@@ -108,7 +108,7 @@ const knot_zone_t *ns_get_zone_for_qname(knot_zonedb_t *zdb,
  * \return The synthetized RRSet (this is a newly created RRSet, remember to
  *         free it).
  */
-static knot_rrset_t *ns_synth_from_wildcard(
+knot_rrset_t *ns_synth_from_wildcard(
 	const knot_rrset_t *wildcard_rrset, const knot_dname_t *qname)
 {
 	knot_rrset_t *rrset = NULL;
@@ -184,7 +184,7 @@ dbg_ns_exec_verb(
  * \return KNOT_ENOMEM
  * \return KNOT_ESPACE
  */
-static int ns_add_rrsigs(knot_rrset_t *rrset, knot_pkt_t *resp,
+int ns_add_rrsigs(knot_rrset_t *rrset, knot_pkt_t *resp,
                          const knot_dname_t *name,
                          uint32_t flags)
 {
@@ -203,13 +203,16 @@ static int ns_add_rrsigs(knot_rrset_t *rrset, knot_pkt_t *resp,
 	        || knot_pkt_qtype(resp) == KNOT_RRTYPE_ANY)
 	    && (rrsigs = knot_rrset_get_rrsigs(rrset)) != NULL) {
 		if (name != NULL) {
+			knot_rrset_t *rrsigs_orig = rrsigs;
 			int ret = ns_check_wildcard(name, resp, &rrsigs);
 			if (ret != KNOT_EOK) {
 				dbg_ns("Failed to process wildcard: %s\n",
 				       knot_strerror(ret));
 				return ret;
 			}
-			/* #10 will leak if synthetized new */
+			if (rrsigs != rrsigs_orig) {
+				flags |= KNOT_PF_FREE;
+			}
 		}
 		return knot_pkt_put(resp, 0, rrsigs, flags|KNOT_PF_CHECKDUP);
 	}
@@ -476,14 +479,20 @@ dbg_ns_exec_verb(
 			if (knot_rrset_rdata_rr_count(rrset) > 0
 			    || knot_rrset_type(rrset) == KNOT_RRTYPE_APL) {
 
+				knot_rrset_t *rrset_orig = rrset;
 				ret = ns_check_wildcard(name, resp, &rrset);
 				if (ret != KNOT_EOK) {
 					dbg_ns("Failed to process wildcard.\n");
 					break;
 				}
 
+				unsigned flags = 0;
+				if (rrset != rrset_orig) {
+					flags |= KNOT_PF_FREE;
+				}
+
 				assert(KNOT_PKT_IN_AN(resp));
-				ret = knot_pkt_put(resp, 0, rrset, 0);
+				ret = knot_pkt_put(resp, 0, rrset, flags);
 				if (ret != KNOT_EOK) {
 					dbg_ns("Failed add Answer RRSet: %s\n",
 					       knot_strerror(ret));
@@ -526,14 +535,20 @@ dbg_ns_exec_verb(
 				continue;
 			}
 
+			knot_rrset_t *rrset_orig = rrset;
 			ret = ns_check_wildcard(name, resp, &rrset);
 			if (ret != KNOT_EOK) {
 				dbg_ns("Failed to process wildcard.\n");
 				break;
 			}
 
+			unsigned flags = 0;
+			if (rrset != rrset_orig) {
+				flags |= KNOT_PF_FREE;
+			}
+
 			assert(KNOT_PKT_IN_AN(resp));
-			ret = knot_pkt_put(resp, 0, rrset, 0);
+			ret = knot_pkt_put(resp, 0, rrset, flags);
 			if (ret != KNOT_EOK) {
 				dbg_ns("Failed add Answer RRSet: %s\n",
 				       knot_strerror(ret));
@@ -553,14 +568,20 @@ dbg_ns_exec_verb(
 		if (rrset != NULL && knot_rrset_rdata_rr_count(rrset)) {
 			dbg_ns_verb("Found RRSet of type %u\n", type);
 
+			knot_rrset_t *rrset2_orig = rrset2;
 			ret = ns_check_wildcard(name, resp, &rrset2);
 			if (ret != KNOT_EOK) {
 				dbg_ns("Failed to process wildcard.\n");
 				break;
 			}
 
+			unsigned flags = 0;
+			if (rrset2 != rrset2_orig) {
+				flags |= KNOT_PF_FREE;
+			}
+
 			assert(KNOT_PKT_IN_AN(resp));
-			ret = knot_pkt_put(resp, 0, rrset2, 0);
+			ret = knot_pkt_put(resp, 0, rrset2, flags);
 			if (ret != KNOT_EOK) {
 				dbg_ns("Failed add Answer RRSet: %s\n",
 				       knot_strerror(ret));
@@ -674,9 +695,14 @@ dbg_ns_exec(
 					return ret;
 				}
 
+				unsigned flags = KNOT_PF_NOTRUNC|KNOT_PF_CHECKDUP;
+				if (rrset_add2 != rrset_add) {
+					flags |= KNOT_PF_FREE;
+				}
+
 				assert(KNOT_PKT_IN_AR(resp));
 				ret = knot_pkt_put(
-					resp, compr_hint, rrset_add2, KNOT_PF_NOTRUNC|KNOT_PF_CHECKDUP);
+					resp, compr_hint, rrset_add2, flags);
 
 				if (ret != KNOT_EOK) {
 					dbg_ns("Failed to add A RRSet to "
@@ -711,9 +737,14 @@ dbg_ns_exec(
 					return ret;
 				}
 
+				unsigned flags = KNOT_PF_NOTRUNC|KNOT_PF_CHECKDUP;
+				if (rrset_add2 != rrset_add) {
+					flags |= KNOT_PF_FREE;
+				}
+
 				assert(KNOT_PKT_IN_AR(resp));
 				ret = knot_pkt_put(
-					resp, compr_hint, rrset_add2, KNOT_PF_NOTRUNC|KNOT_PF_CHECKDUP);
+					resp, compr_hint, rrset_add2, flags);
 
 				if (ret != KNOT_EOK) {
 					dbg_ns("Failed to add AAAA RRSet to "
@@ -4117,6 +4148,10 @@ int ns_proc_out(uint8_t *wire, uint16_t *wire_len, ns_proc_context_t *ctx)
 	}
 
 	*wire_len = pkt->size;
+
+	/* Free packet. */
+	knot_pkt_free(&pkt);
+
 	return ctx->state;
 }
 
diff --git a/src/libknot/nameserver/name-server.h b/src/libknot/nameserver/name-server.h
index d4595098b0219881a177fb1ce2a3c5de2cecb8f9..01457d20bba8e6bbb1d35d189a322764e0c8be90 100644
--- a/src/libknot/nameserver/name-server.h
+++ b/src/libknot/nameserver/name-server.h
@@ -418,6 +418,11 @@ int ns_referral(const knot_node_t *node,
                               knot_pkt_t *resp,
                               uint16_t qtype);
 
+knot_rrset_t *ns_synth_from_wildcard(const knot_rrset_t *wildcard_rrset, const knot_dname_t *qname);
+
+int ns_add_rrsigs(knot_rrset_t *rrset, knot_pkt_t *resp,
+                         const knot_dname_t *name,
+                         uint32_t flags);
 /* #10 >>> Exposed API. */
 
 /* #10 <<< Next-gen API. */
diff --git a/src/libknot/nameserver/ns_proc_query.c b/src/libknot/nameserver/ns_proc_query.c
index a66dfaa49642288ec5ad01db132e61001810b692..078c744a8f2be090fec5e42b0b7f749b7c7d041a 100644
--- a/src/libknot/nameserver/ns_proc_query.c
+++ b/src/libknot/nameserver/ns_proc_query.c
@@ -6,15 +6,17 @@
 #include "common/descriptor.h"
 #include "libknot/common.h"
 #include "libknot/consts.h"
+#include "libknot/rdata.h"
 #include "libknot/util/debug.h"
 #include "libknot/nameserver/chaos.h"
 
 struct query_data {
-	int state;
 	uint16_t rcode;
 	uint16_t rcode_tsig;
 	knot_pkt_t *pkt;
 	const knot_node_t *node, *encloser, *previous;
+	list_t wildcards;
+	mm_ctx_t *mm;
 };
 
 /* Forward decls. */
@@ -44,7 +46,13 @@ int ns_proc_query_begin(ns_proc_context_t *ctx)
 	assert(ctx);
 	ctx->type = NS_PROC_QUERY_ID;
 	ctx->data = ctx->mm.alloc(ctx->mm.ctx, sizeof(struct query_data));
-	memset(ctx->data, 0, sizeof(struct query_data));
+
+	struct query_data *data = QUERY_DATA(ctx);
+	memset(data, 0, sizeof(struct query_data));
+	data->mm = &ctx->mm;
+
+	/* Initialize list. */
+	init_list(&data->wildcards);
 
 	/* Await packet. */
 	return NS_PROC_MORE;
@@ -56,7 +64,12 @@ int ns_proc_query_reset(ns_proc_context_t *ctx)
 	assert(ctx);
 	struct query_data *data = QUERY_DATA(ctx);
 	knot_pkt_free(&data->pkt);
-	memset(data, 0, sizeof(struct query_data));
+	data->rcode = KNOT_RCODE_NOERROR;
+	data->rcode_tsig = 0;
+	data->node = data->encloser = data->previous = NULL;
+
+	/* Free wildcard list. */
+	ptrlist_free(&data->wildcards, data->mm);
 
 	/* Await packet. */
 	return NS_PROC_MORE;
@@ -283,6 +296,90 @@ enum {
 	ERROR
 };
 
+int in_zone_name_cname(knot_pkt_t *pkt, const knot_dname_t **name, struct query_data *qdata)
+{
+	dbg_ns("%s(%p, %p, %p)\n", __func__, name, pkt, qdata);
+
+	const knot_node_t *cname_node = qdata->node;
+	knot_rrset_t *cname_rr = knot_node_get_rrset(qdata->node, KNOT_RRTYPE_CNAME);
+	knot_rrset_t *rr_to_add = cname_rr;
+	unsigned flags = 0;
+	int ret = KNOT_EOK;
+
+	assert(cname_rr != NULL);
+
+	/* Is node a wildcard? */
+	if (knot_dname_is_wildcard(cname_node->owner)) {
+
+		/* Check if is not in wildcard nodes (loop). */
+		dbg_ns("%s: CNAME node %p is wildcard\n", __func__, cname_node);
+		if (ptrlist_contains(&qdata->wildcards, cname_node)) {
+			dbg_ns("%s: node %p already visited => CNAME loop\n",
+			       __func__, cname_node);
+			return HIT;
+		}
+
+		/* Put to wildcard node list. */
+		if (ptrlist_add(&qdata->wildcards, cname_node, qdata->mm) == NULL) {
+			qdata->rcode = KNOT_RCODE_SERVFAIL;
+			return ERROR;
+		}
+
+		/* Synthetic RRSet. */
+		rr_to_add = ns_synth_from_wildcard(cname_rr, *name);
+
+		/* Free RRSet with packet. */
+		flags |= KNOT_PF_FREE;
+
+	} else {
+		/* Normal CNAME name, check for duplicate. */
+		flags |= KNOT_PF_CHECKDUP;
+	}
+
+	/* Now, try to put CNAME to answer. */
+	ret = knot_pkt_put(pkt, 0, rr_to_add, flags);
+	if (ret != KNOT_EOK) {
+		/* Free if synthetized. */
+		if (rr_to_add != cname_rr) {
+			knot_rrset_deep_free(&rr_to_add, 1);
+		}
+		/* Duplicate found, end resolving chain. */
+		if (ret == KNOT_ENORRSET) {
+			dbg_ns("%s: RR %p already inserted => CNAME loop\n",
+			       __func__, rr_to_add);
+			return HIT;
+		} else {
+			qdata->rcode = KNOT_RCODE_SERVFAIL;
+			return ERROR;
+		}
+	}
+
+	/* Add RR signatures (from original RR). */
+	ret = ns_add_rrsigs(cname_rr, pkt, *name, 0);
+	if (ret != KNOT_EOK) {
+		dbg_ns("%s: couldn't add rrsigs for CNAME RRSet %p\n",
+		       __func__, cname_rr);
+		qdata->rcode = KNOT_RCODE_SERVFAIL;
+		return ERROR;
+	}
+
+	/* Now follow the next CNAME TARGET. */
+	*name = knot_rdata_cname_name(cname_rr);
+
+#ifdef KNOT_NS_DEBUG
+	char *cname_str = knot_dname_to_str(cname_node->owner);
+	char *target_str = knot_dname_to_str(*name);
+	dbg_ns("%s: FOLLOW '%s' -> '%s'\n", __func__, cname_str, target_str);
+	free(cname_str);
+	free(target_str);
+#endif /* KNOT_NS_DEBUG */
+
+	/* Invalidate current node. */
+	qdata->node = qdata->encloser = qdata->previous = NULL;
+
+	return FOLLOW;
+}
+
 static int in_zone_name_found(knot_pkt_t *pkt, const knot_dname_t **name,
                               struct query_data *qdata)
 {
@@ -292,9 +389,7 @@ static int in_zone_name_found(knot_pkt_t *pkt, const knot_dname_t **name,
 	if (knot_node_rrset(qdata->node, KNOT_RRTYPE_CNAME) != NULL
 	    && qtype != KNOT_RRTYPE_CNAME && qtype != KNOT_RRTYPE_RRSIG) {
 		dbg_ns("%s: solving CNAME\n", __func__);
-		qdata->rcode = KNOT_RCODE_NOTIMPL;
-		return ERROR;
-		assert(0); /*! \todo Implement CNAME solving. */
+		return in_zone_name_cname(pkt, name, qdata);
 	}
 
 	// now we have the node for answering