diff --git a/Knot.files b/Knot.files index ed4d228bb68b8c5002818a807db9e1a2cec39491..e5a0c40456815bd616505c48b0841f03d4ed0968 100644 --- a/Knot.files +++ b/Knot.files @@ -166,6 +166,8 @@ src/libknot/edns.h src/libknot/libknot.h src/libknot/nameserver/chaos.c src/libknot/nameserver/chaos.h +src/libknot/nameserver/internet.c +src/libknot/nameserver/internet.h src/libknot/nameserver/name-server.c src/libknot/nameserver/name-server.h src/libknot/nameserver/ns_proc_query.c diff --git a/src/Makefile.am b/src/Makefile.am index 610e3e19fdd5ff03adc8148a7ed12d442864285d..48f1b177a6412ab60a6afdc3bc53554ed654a581 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -122,6 +122,8 @@ libknot_la_SOURCES = \ libknot/nameserver/name-server.c \ libknot/nameserver/chaos.h \ libknot/nameserver/chaos.c \ + libknot/nameserver/internet.h \ + libknot/nameserver/internet.c \ libknot/nameserver/ns_proc_query.h \ libknot/nameserver/ns_proc_query.c \ libknot/updates/changesets.h \ diff --git a/src/libknot/nameserver/name-server.h b/src/libknot/nameserver/name-server.h index 4b114e311a8d7de40e69625d63a7da7fbc53ec53..db498369330afe840743e035aa6a45f465a4b8f1 100644 --- a/src/libknot/nameserver/name-server.h +++ b/src/libknot/nameserver/name-server.h @@ -447,12 +447,18 @@ enum ns_proc_state { NS_PROC_FAIL = 1 << 3, }; +enum ns_proc_flag { + NS_NOFLAG = 0, + NS_PKTSIZE_NOLIMIT = 1 << 0, /* Don't limit packet size (for TCP). */ +}; + struct ns_proc_module; typedef struct ns_proc_context { mm_ctx_t mm; uint16_t type; + uint16_t flags; void *data; int state; diff --git a/src/libknot/nameserver/ns_proc_query.c b/src/libknot/nameserver/ns_proc_query.c index 1d79663f3c5aee995c5c29a008022dfb7ecd43c1..83807f8876b89fb83591b4537ae27b13975dfc36 100644 --- a/src/libknot/nameserver/ns_proc_query.c +++ b/src/libknot/nameserver/ns_proc_query.c @@ -3,30 +3,19 @@ #include <urcu.h> #include "libknot/nameserver/ns_proc_query.h" -#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 { - 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; -}; +#include "libknot/nameserver/internet.h" +#include "libknot/common.h" +#include "common/descriptor.h" /* Forward decls. */ -int answer_internet(knot_pkt_t *pkt, ns_proc_context_t *ctx); -int answer_chaos(knot_pkt_t *pkt, ns_proc_context_t *ctx); static int tsig_check(knot_pkt_t *pkt); -static int zone_state(const knot_zone_t *zone); -static const knot_zone_t *find_query_zone(knot_pkt_t *pkt, knot_nameserver_t *ns); -static int prepare_answer(knot_pkt_t *query, knot_pkt_t *resp, knot_nameserver_t *ns); -static int in_zone_answer(knot_pkt_t *resp, struct query_data *qdata); +static const knot_zone_t *answer_zone_find(knot_pkt_t *pkt, knot_zonedb_t *zonedb); +static int prepare_answer(knot_pkt_t *query, knot_pkt_t *resp, ns_proc_context_t *ctx); +int query_internet(knot_pkt_t *pkt, ns_proc_context_t *ctx); +int query_chaos(knot_pkt_t *pkt, ns_proc_context_t *ctx); /*! \brief Module implementation. */ const ns_proc_module_t _ns_proc_query = { @@ -124,7 +113,7 @@ int ns_proc_query_out(knot_pkt_t *pkt, ns_proc_context_t *ctx) /* Prepare answer. */ int next_state = NS_PROC_FINISH; - int ret = prepare_answer(data->pkt, pkt, ctx->ns); + int ret = prepare_answer(data->pkt, pkt, ctx); if (ret != KNOT_EOK) { data->rcode = KNOT_RCODE_SERVFAIL; rcu_read_unlock(); @@ -136,11 +125,11 @@ int ns_proc_query_out(knot_pkt_t *pkt, ns_proc_context_t *ctx) /* Answer based on qclass. */ switch (knot_pkt_qclass(pkt)) { case KNOT_CLASS_CH: - next_state = answer_chaos(pkt, ctx); + next_state = query_chaos(pkt, ctx); break; case KNOT_CLASS_ANY: case KNOT_CLASS_IN: - next_state = answer_internet(pkt, ctx); + next_state = query_internet(pkt, ctx); break; default: data->rcode = KNOT_RCODE_REFUSED; @@ -181,14 +170,14 @@ int ns_proc_query_err(knot_pkt_t *pkt, ns_proc_context_t *ctx) /*! * \brief Create a response for a given query in the CHAOS class. */ -int answer_internet(knot_pkt_t *pkt, ns_proc_context_t *ctx) +int query_internet(knot_pkt_t *pkt, ns_proc_context_t *ctx) { struct query_data *data = QUERY_DATA(ctx); int next_state = NS_PROC_FAIL; /* Check zone validity. */ - switch(zone_state(pkt->zone)) { - case KNOT_EOK: next_state = in_zone_answer(pkt, data); break; + switch(knot_zone_state(pkt->zone)) { + case KNOT_EOK: next_state = internet_answer(pkt, data); break; case KNOT_ENOENT: data->rcode = KNOT_RCODE_REFUSED; break; default: data->rcode = KNOT_RCODE_SERVFAIL; break; } @@ -199,7 +188,7 @@ int answer_internet(knot_pkt_t *pkt, ns_proc_context_t *ctx) /*! * \brief Create a response for a given query in the CHAOS class. */ -int answer_chaos(knot_pkt_t *pkt, ns_proc_context_t *ctx) +int query_chaos(knot_pkt_t *pkt, ns_proc_context_t *ctx) { dbg_ns("%s(%p, %p)\n", __func__, pkt, ctx); struct query_data *data = QUERY_DATA(ctx); @@ -223,19 +212,7 @@ static int tsig_check(knot_pkt_t *pkt) return KNOT_EOK; } -static int zone_state(const knot_zone_t *zone) -{ - if (zone == NULL) { - dbg_ns("%s: zone not found\n", __func__); - return KNOT_ENOENT; - } else if (zone->contents == NULL) { - dbg_ns("%s: zone expired or stub\n", __func__); - return KNOT_ENOZONE; - } - return KNOT_EOK; -} - -static const knot_zone_t *find_query_zone(knot_pkt_t *pkt, knot_nameserver_t *ns) +static const knot_zone_t *answer_zone_find(knot_pkt_t *pkt, knot_zonedb_t *zonedb) { uint16_t qtype = knot_pkt_qtype(pkt); uint16_t qclass = knot_pkt_qclass(pkt); @@ -247,13 +224,12 @@ static const knot_zone_t *find_query_zone(knot_pkt_t *pkt, knot_nameserver_t *ns } // find zone in which to search for the name - knot_zonedb_t *zonedb = rcu_dereference(ns->zone_db); return ns_get_zone_for_qname(zonedb, qname, qtype); } -static int prepare_answer(knot_pkt_t *query, knot_pkt_t *resp, knot_nameserver_t *ns) +static int prepare_answer(knot_pkt_t *query, knot_pkt_t *resp, ns_proc_context_t *ctx) { - dbg_ns("%s(%p, %p, %p)\n", __func__, query, resp, ns); + dbg_ns("%s(%p, %p, %p)\n", __func__, query, resp, ctx); int ret = knot_pkt_init_response(resp, query); if (ret != KNOT_EOK) { @@ -262,326 +238,36 @@ static int prepare_answer(knot_pkt_t *query, knot_pkt_t *resp, knot_nameserver_t } // find zone for qname - resp->zone = find_query_zone(query, ns); - dbg_ns("%s: found zone %p for pkt %p\n", __func__, resp->zone, query); + resp->zone = answer_zone_find(query, ctx->ns->zone_db); + + /* Update maximal answer size. */ + if (!(ctx->flags & NS_PKTSIZE_NOLIMIT)) { + resp->max_size = KNOT_WIRE_MIN_PKTSIZE; + } /* Check if EDNS is supported. */ if (!knot_pkt_have_edns(query)) { + dbg_ns("%s: packet size limit %zuB\n", __func__, resp->max_size); return KNOT_EOK; } - - // set the OPT RR to the response - ret = knot_pkt_add_opt(resp, ns->opt_rr, knot_pkt_have_nsid(query)); - if (ret == KNOT_EOK) { - // copy the DO bit from the query - if (knot_pkt_have_dnssec(query)) { - dbg_ns("%s: setting DO=1 in OPT RR\n", __func__); - knot_edns_set_do(&(resp)->opt_rr); - } - } else { - dbg_ns("%s: can't add OPT RR (%d)\n", __func__, ret); - } - - return ret; -} - -enum { - BEGIN, - NODATA, - HIT, - MISS, - DELEG, - FOLLOW, - 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. */ - uint16_t rr_count_before = pkt->rrset_count; - ret = knot_pkt_put(pkt, 0, rr_to_add, flags); + ret = knot_pkt_add_opt(resp, ctx->ns->opt_rr, knot_pkt_have_nsid(query)); if (ret != KNOT_EOK) { - /* Free if synthetized. */ - if (rr_to_add != cname_rr) { - knot_rrset_deep_free(&rr_to_add, 1); - } - qdata->rcode = KNOT_RCODE_SERVFAIL; - return ERROR; - } else { - /* Check if RR count increased. */ - if (pkt->rrset_count <= rr_count_before) { - dbg_ns("%s: RR %p already inserted => CNAME loop\n", - __func__, rr_to_add); - return HIT; - } - - } - - /* 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 */ - - return FOLLOW; -} - -static int in_zone_name_found(knot_pkt_t *pkt, const knot_dname_t **name, - struct query_data *qdata) -{ - uint16_t qtype = knot_pkt_qtype(pkt); - dbg_ns("%s(%p, %p, %p)\n", __func__, pkt, name, qdata); - - if (knot_node_rrset(qdata->node, KNOT_RRTYPE_CNAME) != NULL - && qtype != KNOT_RRTYPE_CNAME - && qtype != KNOT_RRTYPE_RRSIG - && qtype != KNOT_RRTYPE_ANY) { - dbg_ns("%s: solving CNAME\n", __func__); - return in_zone_name_cname(pkt, name, qdata); - } - - // now we have the node for answering - if (qtype != KNOT_RRTYPE_DS && // DS query is answered normally - (knot_node_is_deleg_point(qdata->node) || knot_node_is_non_auth(qdata->node))) { - dbg_ns("%s: solving REFERRAL\n", __func__); - return DELEG; - } - - int added = 0; /*! \todo useless */ - int ret = ns_put_answer(qdata->node, pkt->zone->contents, *name, qtype, pkt, &added, 0 /*! \todo check from pkt */); - - if (ret != KNOT_EOK) { - dbg_ns("%s: failed answer from node %p (%d)\n", __func__, qdata->node, ret); - /*! \todo set rcode */ - return ERROR; - } else { - dbg_ns("%s: answered, %d added\n", __func__, added); - } - - // this is the only case when the servers answers from - // particular node, i.e. the only case when it may return SOA - // or NS records in Answer section - if (knot_wire_get_tc(pkt->wire) == 0 - && knot_pkt_have_dnssec(pkt->query) - && qdata->node == knot_zone_contents_apex(pkt->zone->contents) - && (qtype == KNOT_RRTYPE_SOA || qtype == KNOT_RRTYPE_NS)) { - ret = ns_add_dnskey(qdata->node, pkt); - if (ret != KNOT_EOK) { - qdata->rcode = KNOT_RCODE_SERVFAIL; - return ERROR; - } - } - - /* Check for NODATA. */ - if (added == 0) { - return NODATA; - } else { - return HIT; - } -} - -static int in_zone_name_not_found(knot_pkt_t *pkt, const knot_dname_t **name, - struct query_data *qdata) -{ - dbg_ns("%s(%p, %p, %p)\n", __func__, pkt, name, qdata); - - /* Name is covered by wildcard. */ - const knot_node_t *wildcard_node = knot_node_wildcard_child(qdata->encloser); - if (wildcard_node) { - dbg_ns("%s: name %p covered by wildcard\n", __func__, *name); - qdata->node = wildcard_node; - qdata->encloser = wildcard_node; - qdata->previous = NULL; - return in_zone_name_found(pkt, name, qdata); - } - - /* Name is under DNAME, use it for substitution. */ - knot_rrset_t *dname_rrset = knot_node_get_rrset(qdata->encloser, KNOT_RRTYPE_DNAME); - if (dname_rrset != NULL - && knot_rrset_rdata_rr_count(dname_rrset) > 0) { - dbg_ns("%s: solving DNAME for name %p\n", __func__, *name); - int ret = ns_process_dname(dname_rrset, name, pkt); - if (ret != KNOT_EOK) { - qdata->rcode = KNOT_RCODE_SERVFAIL; - return ERROR; - } - - return FOLLOW; - } - - dbg_ns("%s: name not found in zone %p\n", __func__, *name); - return MISS; -} - -static int in_zone_solve_name(int state, const knot_dname_t **name, - knot_pkt_t *pkt, struct query_data *qdata) -{ - dbg_ns("%s(%d, %p, %p, %p)\n", __func__, state, name, pkt, qdata); - int ret = knot_zone_contents_find_dname(pkt->zone->contents, *name, - &qdata->node, &qdata->encloser, - &qdata->previous); - - switch(ret) { - case KNOT_ZONE_NAME_FOUND: - return in_zone_name_found(pkt, name, qdata); - case KNOT_ZONE_NAME_NOT_FOUND: - return in_zone_name_not_found(pkt, name, qdata); - case KNOT_EOUTOFZONE: - assert(state == FOLLOW); /* CNAME/DNAME chain only. */ - return HIT; - default: - return ERROR; + dbg_ns("%s: can't add OPT RR (%d)\n", __func__, ret); + return ret; } -} - -static int in_zone_solve_answer(const knot_dname_t **qname, - knot_pkt_t *pkt, struct query_data *qdata) -{ - /* Get answer to QNAME. */ - int state = in_zone_solve_name(BEGIN, qname, pkt, qdata); - /* Is authoritative answer unless referral. - * Must check before we chase the CNAME chain. */ - if (state != DELEG) { - knot_wire_set_aa(pkt->wire); + /* Copy DO bit if set (DNSSEC requested). */ + if (knot_pkt_have_dnssec(query)) { + dbg_ns("%s: setting DO=1 in OPT RR\n", __func__); + knot_edns_set_do(&(resp)->opt_rr); } - - /* Additional resolving for CNAME/DNAME chain. */ - while (state == FOLLOW) { - state = in_zone_solve_name(state, qname, pkt, qdata); - /* Chain lead to NXDOMAIN, this is okay since - * the first CNAME/DNAME is a valid answer. */ - if (state == MISS) { - state = HIT; - } - } - - return state; -} - -static int in_zone_solve_authority(int state, const knot_dname_t **qname, - knot_pkt_t *pkt, struct query_data *qdata) -{ - int ret = KNOT_ERROR; - - switch (state) { - case HIT: /* Positive response, add (optional) AUTHORITY NS. */ - ret = ns_put_authority_ns(pkt->zone->contents, pkt); - dbg_ns("%s: putting authority NS = %d\n", __func__, ret); - break; - case MISS: /* MISS, set NXDOMAIN RCODE. */ - qdata->rcode = KNOT_RCODE_NXDOMAIN; - dbg_ns("%s: answer is NXDOMAIN\n", __func__); - case NODATA: /* NODATA or NXDOMAIN, append AUTHORITY SOA. */ - ret = ns_put_authority_soa(pkt->zone->contents, pkt); - dbg_ns("%s: putting authority SOA = %d\n", __func__, ret); - break; - case DELEG: /* Referral response. */ /*! \todo DS + NS */ - ret = ns_referral(qdata->node, pkt->zone->contents, *qname, pkt, knot_pkt_qtype(pkt)); - break; - case ERROR: - dbg_ns("%s: failed to resolve qname\n", __func__); - break; - default: - dbg_ns("%s: invalid state after qname processing = %d\n", - __func__, state); - assert(0); - qdata->rcode = KNOT_RCODE_SERVFAIL; - break; + /* Set minimal supported size from EDNS(0). */ + if (!(ctx->flags & NS_PKTSIZE_NOLIMIT)) { + uint16_t client_maxlen = knot_edns_get_payload(&query->opt_rr); + uint16_t server_maxlen = knot_edns_get_payload(&resp->opt_rr); + resp->max_size = MAX(resp->max_size, MIN(client_maxlen, server_maxlen)); } + dbg_ns("%s: packet size limit %zuB\n", __func__, resp->max_size); return ret; } - -static int in_zone_answer(knot_pkt_t *resp, struct query_data *qdata) -{ - dbg_ns("%s(%p, %p)\n", __func__, resp, qdata); - - /* Write answer RRs for QNAME. */ - dbg_ns("%s: writing %p ANSWER\n", __func__, resp); - knot_pkt_begin(resp, KNOT_ANSWER); - - const knot_dname_t *qname = knot_pkt_qname(resp); - - /* Get answer to QNAME. */ - int state = in_zone_solve_answer(&qname, resp, qdata); - - /* Resolve AUTHORITY. */ - dbg_ns("%s: writing %p AUTHORITY\n", __func__, resp); - knot_pkt_begin(resp, KNOT_AUTHORITY); - int ret = in_zone_solve_authority(state, &qname, resp, qdata); - if (ret != KNOT_EOK) { - return NS_PROC_FAIL; - - } - - // add all missing NSECs/NSEC3s for wildcard nodes - /*! \todo Make function accept query_data with zone+wcnodes */ - - /* Resolve ADDITIONAL. */ - dbg_ns("%s: writing %p ADDITIONAL\n", __func__, resp); - knot_pkt_begin(resp, KNOT_ADDITIONAL); - ret = ns_put_additional(resp); - if (ret != KNOT_EOK) { - return NS_PROC_FAIL; - - } - - /* Write RCODE. */ - knot_wire_set_rcode(resp->wire, qdata->rcode); - - /* Complete response. */ - return NS_PROC_FINISH; -} diff --git a/src/libknot/nameserver/ns_proc_query.h b/src/libknot/nameserver/ns_proc_query.h index d7a4e2f268857676fbdebe0bd980608138bb7fd2..b03185815876e3afd414d46d15fb1bf01c39d3a8 100644 --- a/src/libknot/nameserver/ns_proc_query.h +++ b/src/libknot/nameserver/ns_proc_query.h @@ -3,9 +3,9 @@ * * \author Marek Vavrusa <marek.vavrusa@nic.cz> * - * \brief Normal query processor. + * \brief Query processor. * - * \addtogroup libknot + * \addtogroup query_processing * @{ */ /* Copyright (C) 2013 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> @@ -24,8 +24,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef _KNOT_NS_PROC_QUERY_H -#define _KNOT_NS_PROC_QUERY_H +#ifndef _KNOT_NS_PROC_QUERY_H_ +#define _KNOT_NS_PROC_QUERY_H_ #include "libknot/nameserver/name-server.h" @@ -33,6 +33,15 @@ extern const ns_proc_module_t _ns_proc_query; #define NS_PROC_QUERY (&_ns_proc_query) #define NS_PROC_QUERY_ID 1 +struct query_data { + 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; +}; + int ns_proc_query_begin(ns_proc_context_t *ctx); int ns_proc_query_reset(ns_proc_context_t *ctx); int ns_proc_query_finish(ns_proc_context_t *ctx); @@ -40,6 +49,6 @@ int ns_proc_query_in(knot_pkt_t *pkt, ns_proc_context_t *ctx); int ns_proc_query_out(knot_pkt_t *pkt, ns_proc_context_t *ctx); int ns_proc_query_err(knot_pkt_t *pkt, ns_proc_context_t *ctx); -#endif /* _KNOT_NS_PROC_QUERY_H */ +#endif /* _KNOT_NS_PROC_QUERY_H_ */ /*! @} */ diff --git a/src/libknot/zone/zone.c b/src/libknot/zone/zone.c index c45849844889f69e1969c01cd689a202908ad07e..8dfa0075b7b9f5d72e71e1064c0ecc50d337f3ad 100644 --- a/src/libknot/zone/zone.c +++ b/src/libknot/zone/zone.c @@ -271,3 +271,15 @@ void knot_zone_set_flag(knot_zone_t *zone, knot_zone_flag_t flag, unsigned on) } } } + +int knot_zone_state(const knot_zone_t *zone) +{ + if (zone == NULL) { + dbg_ns("%s: zone not found\n", __func__); + return KNOT_ENOENT; + } else if (zone->contents == NULL) { + dbg_ns("%s: zone expired or stub\n", __func__); + return KNOT_ENOZONE; + } + return KNOT_EOK; +} diff --git a/src/libknot/zone/zone.h b/src/libknot/zone/zone.h index 90cdf87fcdedfe53d3cb06040a8ef3deefd31926..08b493d5746ade638f74db7de30595cc2124387e 100644 --- a/src/libknot/zone/zone.h +++ b/src/libknot/zone/zone.h @@ -201,6 +201,15 @@ static inline unsigned knot_zone_flags(knot_zone_t *zone) { */ void knot_zone_set_flag(knot_zone_t *zone, knot_zone_flag_t flag, unsigned on); +/*! + * \brief Return zone state. + * \param zone Inspected zone. + * \retval KNOT_EOK if OK + * \retval KNOT_ENOENT if not exists + * \retval KNOT_ENOZONE if expired or stub + */ +int knot_zone_state(const knot_zone_t *zone); + #endif /*! @} */