diff --git a/CHANGES b/CHANGES index e85adfb68e..9435f6ee94 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,8 @@ +5254. [func] Collect metrics to report to the statistics-channel + DNSSEC signing operations (dnssec-sign) and refresh + operations (dnssec-refresh) per zone and per keytag. + [GL #513] + 5253. [port] Support platforms that don't define ULLONG_MAX. [GL #1098] diff --git a/bin/named/statschannel.c b/bin/named/statschannel.c index bd6df21caa..7d9be5d489 100644 --- a/bin/named/statschannel.c +++ b/bin/named/statschannel.c @@ -1444,6 +1444,60 @@ rcodestat_dump(dns_rcode_t code, uint64_t val, void *arg) { #endif } +static void +dnssecsignstat_dump(dns_keytag_t tag, uint64_t val, void *arg) { + FILE *fp; + char tagbuf[64]; + stats_dumparg_t *dumparg = arg; +#ifdef HAVE_LIBXML2 + xmlTextWriterPtr writer; + int xmlrc; +#endif +#ifdef HAVE_JSON_C + json_object *zoneobj, *obj; +#endif + + snprintf(tagbuf, sizeof(tagbuf), "%u", tag); + + switch (dumparg->type) { + case isc_statsformat_file: + fp = dumparg->arg; + fprintf(fp, "%20" PRIu64 " %s\n", val, tagbuf); + break; + case isc_statsformat_xml: +#ifdef HAVE_LIBXML2 + writer = dumparg->arg; + TRY0(xmlTextWriterStartElement(writer, ISC_XMLCHAR "counter")); + TRY0(xmlTextWriterWriteAttribute(writer, ISC_XMLCHAR "name", + ISC_XMLCHAR tagbuf )); + TRY0(xmlTextWriterWriteFormatString(writer, + "%" PRIu64, + val)); + TRY0(xmlTextWriterEndElement(writer)); /* counter */ +#endif + break; + case isc_statsformat_json: +#ifdef HAVE_JSON_C + zoneobj = (json_object *) dumparg->arg; + obj = json_object_new_int64(val); + if (obj == NULL) { + return; + } + json_object_object_add(zoneobj, tagbuf, obj); +#endif + break; + } + return; +#ifdef HAVE_LIBXML2 + error: + isc_log_write(named_g_lctx, NAMED_LOGCATEGORY_GENERAL, + NAMED_LOGMODULE_SERVER, ISC_LOG_ERROR, + "failed at dnssecsignstat_dump()"); + dumparg->result = ISC_R_FAILURE; + return; +#endif +} + #ifdef HAVE_LIBXML2 /* * Which statistics to include when rendering to XML @@ -1506,6 +1560,8 @@ zone_xmlrender(dns_zone_t *zone, void *arg) { isc_stats_t *zonestats; isc_stats_t *gluecachestats; dns_stats_t *rcvquerystats; + dns_stats_t *dnssecsignstats; + dns_stats_t *dnssecrefreshstats; uint64_t nsstat_values[ns_statscounter_max]; uint64_t gluecachestats_values[dns_gluecachestatscounter_max]; @@ -1533,8 +1589,8 @@ zone_xmlrender(dns_zone_t *zone, void *arg) { TRY0(xmlTextWriterStartElement(writer, ISC_XMLCHAR "counters")); TRY0(xmlTextWriterWriteAttribute(writer, - ISC_XMLCHAR "type", - ISC_XMLCHAR "gluecache")); + ISC_XMLCHAR "type", + ISC_XMLCHAR "gluecache")); result = dump_counters(gluecachestats, isc_statsformat_xml, @@ -1567,6 +1623,46 @@ zone_xmlrender(dns_zone_t *zone, void *arg) { /* counters type="qtype"*/ TRY0(xmlTextWriterEndElement(writer)); } + + dnssecsignstats = dns_zone_getdnssecsignstats(zone); + if (dnssecsignstats != NULL) { + TRY0(xmlTextWriterStartElement(writer, + ISC_XMLCHAR "counters")); + TRY0(xmlTextWriterWriteAttribute(writer, + ISC_XMLCHAR "type", + ISC_XMLCHAR "dnssec-sign")); + + dumparg.result = ISC_R_SUCCESS; + dns_dnssecsignstats_dump(dnssecsignstats, + dnssecsignstat_dump, + &dumparg, 0); + if(dumparg.result != ISC_R_SUCCESS) { + goto error; + } + + /* counters type="dnssec-sign"*/ + TRY0(xmlTextWriterEndElement(writer)); + } + + dnssecrefreshstats = dns_zone_getdnssecrefreshstats(zone); + if (dnssecrefreshstats != NULL) { + TRY0(xmlTextWriterStartElement(writer, + ISC_XMLCHAR "counters")); + TRY0(xmlTextWriterWriteAttribute(writer, + ISC_XMLCHAR "type", + ISC_XMLCHAR "dnssec-refresh")); + + dumparg.result = ISC_R_SUCCESS; + dns_dnssecsignstats_dump(dnssecrefreshstats, + dnssecsignstat_dump, + &dumparg, 0); + if(dumparg.result != ISC_R_SUCCESS) { + goto error; + } + + /* counters type="dnssec-refresh"*/ + TRY0(xmlTextWriterEndElement(writer)); + } } TRY0(xmlTextWriterEndElement(writer)); /* zone */ @@ -2287,6 +2383,8 @@ zone_jsonrender(dns_zone_t *zone, void *arg) { isc_stats_t *zonestats; isc_stats_t *gluecachestats; dns_stats_t *rcvquerystats; + dns_stats_t *dnssecsignstats; + dns_stats_t *dnssecrefreshstats; uint64_t nsstat_values[ns_statscounter_max]; uint64_t gluecachestats_values[dns_gluecachestatscounter_max]; @@ -2364,6 +2462,58 @@ zone_jsonrender(dns_zone_t *zone, void *arg) { else json_object_put(counters); } + + dnssecsignstats = dns_zone_getdnssecsignstats(zone); + if (dnssecsignstats != NULL) { + stats_dumparg_t dumparg; + json_object *counters = json_object_new_object(); + CHECKMEM(counters); + + dumparg.type = isc_statsformat_json; + dumparg.arg = counters; + dumparg.result = ISC_R_SUCCESS; + dns_dnssecsignstats_dump(dnssecsignstats, + dnssecsignstat_dump, + &dumparg, 0); + if (dumparg.result != ISC_R_SUCCESS) { + json_object_put(counters); + goto error; + } + + if (json_object_get_object(counters)->count != 0) { + json_object_object_add(zoneobj, + "dnssec-sign", + counters); + } else { + json_object_put(counters); + } + } + + dnssecrefreshstats = dns_zone_getdnssecrefreshstats(zone); + if (dnssecrefreshstats != NULL) { + stats_dumparg_t dumparg; + json_object *counters = json_object_new_object(); + CHECKMEM(counters); + + dumparg.type = isc_statsformat_json; + dumparg.arg = counters; + dumparg.result = ISC_R_SUCCESS; + dns_dnssecsignstats_dump(dnssecrefreshstats, + dnssecsignstat_dump, + &dumparg, 0); + if (dumparg.result != ISC_R_SUCCESS) { + json_object_put(counters); + goto error; + } + + if (json_object_get_object(counters)->count != 0) { + json_object_object_add(zoneobj, + "dnssec-refresh", + counters); + } else { + json_object_put(counters); + } + } } json_object_array_add(zonearray, zoneobj); diff --git a/bin/named/zoneconf.c b/bin/named/zoneconf.c index c644989e32..8f1da7ca8b 100644 --- a/bin/named/zoneconf.c +++ b/bin/named/zoneconf.c @@ -904,6 +904,8 @@ named_zone_configure(const cfg_obj_t *config, const cfg_obj_t *vconfig, const dns_master_style_t *masterstyle = &dns_master_style_default; isc_stats_t *zoneqrystats; dns_stats_t *rcvquerystats; + dns_stats_t *dnssecsignstats; + dns_stats_t *dnssecrefreshstats; dns_zonestat_level_t statlevel = dns_zonestat_none; int seconds; dns_zone_t *mayberaw = (raw != NULL) ? raw : zone; @@ -1188,14 +1190,19 @@ named_zone_configure(const cfg_obj_t *config, const cfg_obj_t *vconfig, zoneqrystats = NULL; rcvquerystats = NULL; + dnssecsignstats = NULL; + dnssecrefreshstats = NULL; if (statlevel == dns_zonestat_full) { RETERR(isc_stats_create(mctx, &zoneqrystats, ns_statscounter_max)); - RETERR(dns_rdatatypestats_create(mctx, - &rcvquerystats)); + RETERR(dns_rdatatypestats_create(mctx, &rcvquerystats)); + RETERR(dns_dnssecsignstats_create(mctx, &dnssecsignstats)); + RETERR(dns_dnssecsignstats_create(mctx, &dnssecrefreshstats)); } dns_zone_setrequeststats(zone, zoneqrystats); dns_zone_setrcvquerystats(zone, rcvquerystats); + dns_zone_setdnssecsignstats(zone, dnssecsignstats); + dns_zone_setdnssecrefreshstats(zone, dnssecrefreshstats); if (zoneqrystats != NULL) isc_stats_detach(&zoneqrystats); @@ -1203,6 +1210,14 @@ named_zone_configure(const cfg_obj_t *config, const cfg_obj_t *vconfig, if(rcvquerystats != NULL) dns_stats_detach(&rcvquerystats); + if(dnssecsignstats != NULL) { + dns_stats_detach(&dnssecsignstats); + } + + if(dnssecrefreshstats != NULL) { + dns_stats_detach(&dnssecrefreshstats); + } + /* * Configure master functionality. This applies * to primary masters (type "master") and slaves diff --git a/bin/tests/system/statschannel/clean.sh b/bin/tests/system/statschannel/clean.sh index 224e14f76f..f38d6ac0b1 100644 --- a/bin/tests/system/statschannel/clean.sh +++ b/bin/tests/system/statschannel/clean.sh @@ -9,14 +9,18 @@ # See the COPYRIGHT file distributed with this work for additional # information regarding copyright ownership. -rm -f traffic traffic.out.* +rm -f traffic traffic.out.* traffic.json.* traffic.xml.* +rm -f zones zones.out.* zones.json.* zones.xml.* zones.expect.* rm -f dig.out* rm -f */named.memstats rm -f */named.conf -rm -f */named.run +rm -f */named.run* rm -f ns*/named.lock rm -f ns*/named.stats rm -f xml.*stats json.*stats rm -f xml.*mem json.*mem rm -f compressed.headers regular.headers compressed.out regular.out rm -f ns*/managed-keys.bind* +rm -f ns2/Kdnssec* ns2/dnssec.*.id +rm -f ns2/dnssec.db.signed* ns2/dsset-dnssec. +rm -f ns2/core diff --git a/bin/tests/system/statschannel/ns2/dnssec.db.in b/bin/tests/system/statschannel/ns2/dnssec.db.in new file mode 100644 index 0000000000..47cb88e3dc --- /dev/null +++ b/bin/tests/system/statschannel/ns2/dnssec.db.in @@ -0,0 +1,26 @@ +; Copyright (C) Internet Systems Consortium, Inc. ("ISC") +; +; This Source Code Form is subject to the terms of the Mozilla Public +; License, v. 2.0. If a copy of the MPL was not distributed with this +; file, You can obtain one at http://mozilla.org/MPL/2.0/. +; +; See the COPYRIGHT file distributed with this work for additional +; information regarding copyright ownership. + +$ORIGIN . +$TTL 300 + +dnssec. IN SOA mname1. . ( + 1 ; serial + 20 ; refresh (20 seconds) + 20 ; retry (20 seconds) + 1814400 ; expire (3 weeks) + 3600 ; minimum (1 hour) + ) +dnssec. NS ns2.dnssec. +ns2.dnssec. A 10.53.0.2 + +$ORIGIN dnssec. +a A 10.0.0.1 + MX 10 mail.dnssec. +mail A 10.0.0.2 diff --git a/bin/tests/system/statschannel/ns2/named.conf.in b/bin/tests/system/statschannel/ns2/named.conf.in index 9030802f32..fc2952fa2a 100644 --- a/bin/tests/system/statschannel/ns2/named.conf.in +++ b/bin/tests/system/statschannel/ns2/named.conf.in @@ -39,3 +39,13 @@ zone "example" { file "example.db"; allow-transfer { any; }; }; + +zone "dnssec" { + type master; + file "dnssec.db.signed"; + auto-dnssec maintain; + allow-update { any; }; + zone-statistics full; + dnssec-dnskey-kskonly yes; + update-check-ksk yes; +}; diff --git a/bin/tests/system/statschannel/ns2/sign.sh b/bin/tests/system/statschannel/ns2/sign.sh new file mode 100644 index 0000000000..6c275989aa --- /dev/null +++ b/bin/tests/system/statschannel/ns2/sign.sh @@ -0,0 +1,28 @@ +#!/bin/sh -e +# +# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# See the COPYRIGHT file distributed with this work for additional +# information regarding copyright ownership. + +# shellcheck source=conf.sh +. "$SYSTEMTESTTOP/conf.sh" + +set -e + +zone=dnssec. +infile=dnssec.db.in +zonefile=dnssec.db.signed + +ksk=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" -f KSK "$zone") +zsk=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" "$zone") +# Sign deliberately with a very short expiration date. +"$SIGNER" -S -x -O full -e "now"+1s -o "$zone" -f "$zonefile" "$infile" > /dev/null 2>&1 + +echo "$ksk" | sed -e 's/.*[+]//' -e 's/^0*//' > dnssec.ksk.id +echo "$zsk" | sed -e 's/.*[+]//' -e 's/^0*//' > dnssec.zsk.id + diff --git a/bin/tests/system/statschannel/setup.sh b/bin/tests/system/statschannel/setup.sh index e7ad741204..bd9a25cb00 100644 --- a/bin/tests/system/statschannel/setup.sh +++ b/bin/tests/system/statschannel/setup.sh @@ -9,9 +9,14 @@ # See the COPYRIGHT file distributed with this work for additional # information regarding copyright ownership. -SYSTEMTESTTOP=.. -. $SYSTEMTESTTOP/conf.sh +# shellcheck source=conf.sh +. "$SYSTEMTESTTOP/conf.sh" $SHELL clean.sh copy_setports ns2/named.conf.in ns2/named.conf + +( + cd ns2 + $SHELL sign.sh +) diff --git a/bin/tests/system/statschannel/tests.sh b/bin/tests/system/statschannel/tests.sh index 2883b2419a..a0147ec455 100644 --- a/bin/tests/system/statschannel/tests.sh +++ b/bin/tests/system/statschannel/tests.sh @@ -9,8 +9,8 @@ # See the COPYRIGHT file distributed with this work for additional # information regarding copyright ownership. -SYSTEMTESTTOP=.. -. $SYSTEMTESTTOP/conf.sh +# shellcheck source=conf.sh +. "$SYSTEMTESTTOP/conf.sh" DIGCMD="$DIG @10.53.0.2 -p ${PORT}" RNDCCMD="$RNDC -c $SYSTEMTESTTOP/common/rndc.conf -p ${CONTROLPORT} -s" @@ -54,12 +54,48 @@ gettraffic() { *) return 1 ;; esac file=`$PERL fetch.pl -p ${EXTRAPORT1} $path` + cp $file $file.$1.$2 $PERL traffic-${1}.pl $file 2>/dev/null | sort > traffic.out.$2 result=$? - rm -f $file return $result } +getzones() { + sleep 1 + echo_i "... using $1" + case $1 in + xml) path='xml/v3/zones' ;; + json) path='json/v1/zones' ;; + *) return 1 ;; + esac + file=`$PERL fetch.pl -p ${EXTRAPORT1} $path` + cp $file $file.$1.$2 + $PERL zones-${1}.pl $file 2>/dev/null | sort > zones.out.$2 + result=$? + return $result +} + +# TODO: Move wait_for_log and loadkeys_on to conf.sh.common +wait_for_log() { + msg=$1 + file=$2 + + for i in 1 2 3 4 5 6 7 8 9 10; do + nextpart "$file" | grep "$msg" > /dev/null && return + sleep 1 + done + echo_i "exceeded time limit waiting for '$msg' in $file" + ret=1 +} + +loadkeys_on() { + nsidx=$1 + zone=$2 + nextpart ns${nsidx}/named.run > /dev/null + $RNDCCMD 10.53.0.${nsidx} loadkeys ${zone} | sed "s/^/ns${nsidx} /" | cat_i + wait_for_log "next key event" ns${nsidx}/named.run +} + status=0 n=1 ret=0 @@ -243,5 +279,101 @@ if [ $ret != 0 ]; then echo_i "failed"; fi status=`expr $status + $ret` n=`expr $n + 1` +# Test dnssec sign statistics. +zone="dnssec" +sign_prefix="dnssec-sign operations" +refresh_prefix="dnssec-refresh operations" +ksk_id=`cat ns2/$zone.ksk.id` +zsk_id=`cat ns2/$zone.zsk.id` + +# 1. Test sign operations for scheduled resigning. +ret=0 +# The dnssec zone has 10 RRsets to sign (including NSEC) with the ZSK and one +# RRset (DNSKEY) with the KSK. So starting named with signatures that expire +# almost right away, this should trigger 10 zsk and 1 ksk sign operations. +# However, the DNSSEC maintenance assumes when we see the SOA record we have +# walked the whole zone, since the SOA record should always have the most +# recent signature. This however is not always the case, for example when +# the signature expiration is the same, `dns_db_getsigningtime could return +# the SOA RRset before a competing RRset. This happens here and so the +# SOA RRset is updated and resigned twice at startup, that explains the +# additional zsk sign operation (11 instead of 10). +echo "${refresh_prefix} ${zsk_id}: 11" > zones.expect +echo "${refresh_prefix} ${ksk_id}: 1" >> zones.expect +echo "${sign_prefix} ${zsk_id}: 11" >> zones.expect +echo "${sign_prefix} ${ksk_id}: 1" >> zones.expect +cat zones.expect | sort > zones.expect.$n +rm -f zones.expect +# Fetch and check the dnssec sign statistics. +echo_i "fetching zone stats data after zone maintenance at startup ($n)" +if [ $PERL_XML ]; then + getzones xml x$n || ret=1 + cmp zones.out.x$n zones.expect.$n || ret=1 +fi +if [ $PERL_JSON ]; then + getzones json j$n || ret=1 + cmp zones.out.j$n zones.expect.$n || ret=1 +fi +if [ $ret != 0 ]; then echo_i "failed"; fi +status=`expr $status + $ret` +n=`expr $n + 1` + +# 2. Test sign operations after dynamic update. +ret=0 +( +# Update dnssec zone to trigger signature creation. +echo zone $zone +echo server 10.53.0.2 "$PORT" +echo update add $zone. 300 in txt "nsupdate added me" +echo send +) | $NSUPDATE +# This should trigger the resign of SOA, TXT and NSEC (+3 zsk). +echo "${refresh_prefix} ${zsk_id}: 11" > zones.expect +echo "${refresh_prefix} ${ksk_id}: 1" >> zones.expect +echo "${sign_prefix} ${zsk_id}: 14" >> zones.expect +echo "${sign_prefix} ${ksk_id}: 1" >> zones.expect +cat zones.expect | sort > zones.expect.$n +rm -f zones.expect +# Fetch and check the dnssec sign statistics. +echo_i "fetching zone stats data after dynamic update ($n)" +if [ $PERL_XML ]; then + getzones xml x$n || ret=1 + cmp zones.out.x$n zones.expect.$n || ret=1 +fi +if [ $PERL_JSON ]; then + getzones json j$n || ret=1 + cmp zones.out.j$n zones.expect.$n || ret=1 +fi +if [ $ret != 0 ]; then echo_i "failed"; fi +status=`expr $status + $ret` +n=`expr $n + 1` + +# 3. Test sign operations of KSK. +ret=0 +echo_i "fetch zone stats data after updating DNSKEY RRset ($n)" +# Add a standby DNSKEY, this triggers resigning the DNSKEY RRset. +zsk=$("$KEYGEN" -K ns2 -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" "$zone") +$SETTIME -K ns2 -P now -A never $zsk.key > /dev/null +loadkeys_on 2 $zone || ret=1 +# This should trigger the resign of SOA (+1 zsk) and DNSKEY (+1 ksk). +echo "${refresh_prefix} ${zsk_id}: 12" > zones.expect +echo "${refresh_prefix} ${ksk_id}: 2" >> zones.expect +echo "${sign_prefix} ${zsk_id}: 15" >> zones.expect +echo "${sign_prefix} ${ksk_id}: 2" >> zones.expect +cat zones.expect | sort > zones.expect.$n +rm -f zones.expect +# Fetch and check the dnssec sign statistics. +if [ $PERL_XML ]; then + getzones xml x$n || ret=1 + cmp zones.out.x$n zones.expect.$n || ret=1 +fi +if [ $PERL_JSON ]; then + getzones json j$n || ret=1 + cmp zones.out.j$n zones.expect.$n || ret=1 +fi +if [ $ret != 0 ]; then echo_i "failed"; fi +status=`expr $status + $ret` +n=`expr $n + 1` + echo_i "exit status: $status" [ $status -eq 0 ] || exit 1 diff --git a/bin/tests/system/statschannel/zones-json.pl b/bin/tests/system/statschannel/zones-json.pl new file mode 100644 index 0000000000..deef35b0f1 --- /dev/null +++ b/bin/tests/system/statschannel/zones-json.pl @@ -0,0 +1,35 @@ +#!/usr/bin/perl +# +# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# See the COPYRIGHT file distributed with this work for additional +# information regarding copyright ownership. + +# zones-json.pl: +# Parses the JSON version of the dnssec sign stats for the +# "dnssec" zone in the default view into a normalized format. + +use JSON; + +my $file = $ARGV[0]; +open(INPUT, "<$file"); +my $text = do{local$/;}; +close(INPUT); + +my $ref = decode_json($text); + + +my $dnssecsign = $ref->{views}->{_default}->{zones}[0]->{"dnssec-sign"}; +my $type = "dnssec-sign operations "; +foreach $key (keys %{$dnssecsign}) { + print $type . $key . ": ". $dnssecsign->{$key} ."\n"; +} +my $dnssecrefresh = $ref->{views}->{_default}->{zones}[0]->{"dnssec-refresh"}; +my $type = "dnssec-refresh operations "; +foreach $key (keys %{$dnssecrefresh}) { + print $type . $key . ": ". $dnssecrefresh->{$key} ."\n"; +} diff --git a/bin/tests/system/statschannel/zones-xml.pl b/bin/tests/system/statschannel/zones-xml.pl new file mode 100644 index 0000000000..f078b054bb --- /dev/null +++ b/bin/tests/system/statschannel/zones-xml.pl @@ -0,0 +1,37 @@ +#!/usr/bin/perl +# +# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# See the COPYRIGHT file distributed with this work for additional +# information regarding copyright ownership. + +# zones-xml.pl: +# Parses the XML version of the dnssec sign stats for the +# "dnssec" zone in the default view into a normalized format. + +use XML::Simple; + +my $file = $ARGV[0]; + +my $ref = XMLin($file); + +my $counters = $ref->{views}->{view}->{_default}->{zones}->{zone}->{dnssec}->{counters}; + +foreach $group (@$counters) { + + my $type = $group->{type}; + + if ($type eq "dnssec-sign" || $type eq "dnssec-refresh") { + if (exists $group->{counter}->{name}) { + print $type . " operations " . $group->{counter}->{name} . ": " . $group->{counter}->{content} . "\n"; + } else { + foreach $key (keys %{$group->{counter}}) { + print $type . " operations " . $key . ": ". $group->{counter}->{$key}->{content} ."\n"; + } + } + } +} diff --git a/doc/arm/Bv9ARM-book.xml b/doc/arm/Bv9ARM-book.xml index 4a27aa8024..77b8adde78 100644 --- a/doc/arm/Bv9ARM-book.xml +++ b/doc/arm/Bv9ARM-book.xml @@ -5425,6 +5425,8 @@ options { zone-statistics terse or zone-statistics none in the zone statement). + These include, for example, DNSSEC signing operations + and the number of authoritative answers per query type. The default is terse, providing minimal statistics on zones (including name and current serial number, but not query type diff --git a/doc/arm/notes.xml b/doc/arm/notes.xml index c51873b8fc..275e6489d5 100644 --- a/doc/arm/notes.xml +++ b/doc/arm/notes.xml @@ -143,6 +143,20 @@ [GL #865] + + + Two new metrics have been added to the + statistics-channel to report DNSSEC + signing operations. For each key in each zone, the + dnssec-sign counter indicates the total + number of signatures named has generated + using that key since server startup, and the + dnssec-refresh counter indicates how + many of those signatures were refreshed during zone + maintenance, as opposed to having been generated + as a result of a zone update. [GL #513] + + diff --git a/lib/dns/include/dns/stats.h b/lib/dns/include/dns/stats.h index 6b7c423976..52bab5378e 100644 --- a/lib/dns/include/dns/stats.h +++ b/lib/dns/include/dns/stats.h @@ -499,8 +499,8 @@ typedef void (*dns_generalstats_dumper_t)(isc_statscounter_t, uint64_t, void *); typedef void (*dns_rdatatypestats_dumper_t)(dns_rdatastatstype_t, uint64_t, void *); +typedef void (*dns_dnssecsignstats_dumper_t)(dns_keytag_t, uint64_t, void *); typedef void (*dns_opcodestats_dumper_t)(dns_opcode_t, uint64_t, void *); - typedef void (*dns_rcodestats_dumper_t)(dns_rcode_t, uint64_t, void *); ISC_LANG_BEGINDECLS @@ -588,6 +588,22 @@ dns_rcodestats_create(isc_mem_t *mctx, dns_stats_t **statsp); *\li anything else -- failure */ +isc_result_t +dns_dnssecsignstats_create(isc_mem_t *mctx, dns_stats_t **statsp); +/*%< + * Create a statistics counter structure per assigned DNSKEY id. + * + * Requires: + *\li 'mctx' must be a valid memory context. + * + *\li 'statsp' != NULL && '*statsp' == NULL. + * + * Returns: + *\li ISC_R_SUCCESS -- all ok + * + *\li anything else -- failure + */ + void dns_stats_attach(dns_stats_t *stats, dns_stats_t **statsp); /*%< @@ -669,6 +685,15 @@ dns_rcodestats_increment(dns_stats_t *stats, dns_opcode_t code); *\li 'stats' is a valid dns_stats_t created by dns_rcodestats_create(). */ +void +dns_dnssecsignstats_increment(dns_stats_t *stats, dns_keytag_t id); +/*%< + * Increment the statistics counter for the DNSKEY 'id'. + * + * Requires: + *\li 'stats' is a valid dns_stats_t created by dns_dnssecsignstats_create(). + */ + void dns_generalstats_dump(dns_stats_t *stats, dns_generalstats_dumper_t dump_fn, void *arg, unsigned int options); @@ -713,6 +738,21 @@ dns_rdatasetstats_dump(dns_stats_t *stats, dns_rdatatypestats_dumper_t dump_fn, *\li 'stats' is a valid dns_stats_t created by dns_generalstats_create(). */ +void +dns_dnssecsignstats_dump(dns_stats_t *stats, + dns_dnssecsignstats_dumper_t dump_fn, + void *arg, unsigned int options); +/*%< + * Dump the current statistics counters in a specified way. For each counter + * in stats, dump_fn is called with the corresponding type in the form of + * dns_rdatastatstype_t, the current counter value and the given argument + * arg. By default counters that have a value of 0 is skipped; if options has + * the ISC_STATSDUMP_VERBOSE flag, even such counters are dumped. + * + * Requires: + *\li 'stats' is a valid dns_stats_t created by dns_generalstats_create(). + */ + void dns_opcodestats_dump(dns_stats_t *stats, dns_opcodestats_dumper_t dump_fn, void *arg, unsigned int options); diff --git a/lib/dns/include/dns/zone.h b/lib/dns/include/dns/zone.h index e2ad6314a5..33c1131f7f 100644 --- a/lib/dns/include/dns/zone.h +++ b/lib/dns/include/dns/zone.h @@ -1925,6 +1925,12 @@ dns_zone_setrequeststats(dns_zone_t *zone, isc_stats_t *stats); void dns_zone_setrcvquerystats(dns_zone_t *zone, dns_stats_t *stats); + +void +dns_zone_setdnssecsignstats(dns_zone_t *zone, dns_stats_t *stats); + +void +dns_zone_setdnssecrefreshstats(dns_zone_t *zone, dns_stats_t *stats); /*%< * Set additional statistics sets to zone. These are attached to the zone * but are not counted in the zone module; only the caller updates the @@ -1941,6 +1947,12 @@ dns_zone_getrequeststats(dns_zone_t *zone); dns_stats_t * dns_zone_getrcvquerystats(dns_zone_t *zone); + +dns_stats_t * +dns_zone_getdnssecsignstats(dns_zone_t *zone); + +dns_stats_t * +dns_zone_getdnssecrefreshstats(dns_zone_t *zone); /*%< * Get the additional statistics for zone, if one is installed. * @@ -1952,6 +1964,17 @@ dns_zone_getrcvquerystats(dns_zone_t *zone); * otherwise NULL. */ +/*%< + * Set additional statistics sets to zone. These are attached to the zone + * but are not counted in the zone module; only the caller updates the + * counters. + * + * Requires: + * \li 'zone' to be a valid zone. + * + *\li stats is a valid statistics. + */ + void dns_zone_dialup(dns_zone_t *zone); /*%< diff --git a/lib/dns/stats.c b/lib/dns/stats.c index 65123ea198..c5f9121320 100644 --- a/lib/dns/stats.c +++ b/lib/dns/stats.c @@ -35,7 +35,8 @@ typedef enum { dns_statstype_rdtype = 1, dns_statstype_rdataset = 2, dns_statstype_opcode = 3, - dns_statstype_rcode = 4 + dns_statstype_rcode = 4, + dns_statstype_dnssec = 5 } dns_statstype_t; /*% @@ -58,7 +59,8 @@ enum { rdtypecounter_nxdomain = rdtypenxcounter_max, /* stale counters offset */ rdtypecounter_stale = rdtypecounter_nxdomain + 1, - rdatasettypecounter_max = rdtypecounter_stale * 2 + rdatasettypecounter_max = rdtypecounter_stale * 2, + dnssec_keyid_max = 65535 }; struct dns_stats { @@ -84,9 +86,13 @@ typedef struct opcodedumparg { } opcodedumparg_t; typedef struct rcodedumparg { - dns_rcodestats_dumper_t fn; + dns_rcodestats_dumper_t fn; void *arg; } rcodedumparg_t; +typedef struct dnssecsigndumparg { + dns_dnssecsignstats_dumper_t fn; + void *arg; +} dnssecsigndumparg_t; void dns_stats_attach(dns_stats_t *stats, dns_stats_t **statsp) { @@ -196,6 +202,14 @@ dns_rcodestats_create(isc_mem_t *mctx, dns_stats_t **statsp) { dns_rcode_badcookie + 1, statsp)); } +isc_result_t +dns_dnssecsignstats_create(isc_mem_t *mctx, dns_stats_t **statsp) { + REQUIRE(statsp != NULL && *statsp == NULL); + + return (create_stats(mctx, dns_statstype_dnssec, + dnssec_keyid_max, statsp)); +} + /*% * Increment/Decrement methods */ @@ -294,6 +308,13 @@ dns_rcodestats_increment(dns_stats_t *stats, dns_rcode_t code) { isc_stats_increment(stats->counters, (isc_statscounter_t)code); } +void +dns_dnssecsignstats_increment(dns_stats_t *stats, dns_keytag_t id) { + REQUIRE(DNS_STATS_VALID(stats) && stats->type == dns_statstype_dnssec); + + isc_stats_increment(stats->counters, (isc_statscounter_t)id); +} + /*% * Dump methods */ @@ -395,6 +416,28 @@ dns_rdatasetstats_dump(dns_stats_t *stats, dns_rdatatypestats_dumper_t dump_fn, isc_stats_dump(stats->counters, rdataset_dumpcb, &arg, options); } +static void +dnssec_dumpcb(isc_statscounter_t counter, uint64_t value, void *arg) { + dnssecsigndumparg_t *dnssecarg = arg; + + dnssecarg->fn((dns_keytag_t)counter, value, dnssecarg->arg); +} + +void +dns_dnssecsignstats_dump(dns_stats_t *stats, + dns_dnssecsignstats_dumper_t dump_fn, + void *arg0, unsigned int options) +{ + dnssecsigndumparg_t arg; + + REQUIRE(DNS_STATS_VALID(stats) && + stats->type == dns_statstype_dnssec); + + arg.fn = dump_fn; + arg.arg = arg0; + isc_stats_dump(stats->counters, dnssec_dumpcb, &arg, options); +} + static void opcode_dumpcb(isc_statscounter_t counter, uint64_t value, void *arg) { opcodedumparg_t *opcodearg = arg; diff --git a/lib/dns/update.c b/lib/dns/update.c index 745256bf30..e95cc0f84d 100644 --- a/lib/dns/update.c +++ b/lib/dns/update.c @@ -48,6 +48,7 @@ #include #include #include +#include #include #include #include @@ -1076,6 +1077,7 @@ add_sigs(dns_update_log_t *log, dns_zone_t *zone, dns_db_t *db, dns_dbnode_t *node = NULL; dns_rdataset_t rdataset; dns_rdata_t sig_rdata = DNS_RDATA_INIT; + dns_stats_t* dnssecsignstats = dns_zone_getdnssecsignstats(zone); isc_buffer_t buffer; unsigned char data[1024]; /* XXX */ unsigned int i, j; @@ -1181,6 +1183,11 @@ add_sigs(dns_update_log_t *log, dns_zone_t *zone, dns_db_t *db, dns_rdata_reset(&sig_rdata); isc_buffer_init(&buffer, data, sizeof(data)); added_sig = true; + /* Update DNSSEC sign statistics. */ + if (dnssecsignstats != NULL) { + dns_dnssecsignstats_increment(dnssecsignstats, + dst_key_id(keys[i])); + } } if (!added_sig) { update_log(log, zone, ISC_LOG_ERROR, diff --git a/lib/dns/win32/libdns.def.in b/lib/dns/win32/libdns.def.in index a178184060..b3936260c1 100644 --- a/lib/dns/win32/libdns.def.in +++ b/lib/dns/win32/libdns.def.in @@ -331,6 +331,10 @@ dns_dnssec_verify dns_dnssec_verifymessage dns_dnsseckey_create dns_dnsseckey_destroy +dns_dnssecsignstats_create +dns_dnssecsignstats_decrement +dns_dnssecsignstats_dump +dns_dnssecsignstats_increment dns_ds_buildrdata dns_dsdigest_format dns_dsdigest_fromtext @@ -1137,6 +1141,8 @@ dns_zone_getchecknames dns_zone_getclass dns_zone_getdb dns_zone_getdbtype +dns_zone_getdnssecrefreshstats +dns_zone_getdnssecsignstats dns_zone_getexpiretime dns_zone_getfile dns_zone_getforwardacl @@ -1237,6 +1243,8 @@ dns_zone_setclass dns_zone_setdb dns_zone_setdbtype dns_zone_setdialup +dns_zone_setdnssecrefreshstats +dns_zone_setdnssecsignstats dns_zone_setfile dns_zone_setflag dns_zone_setforwardacl diff --git a/lib/dns/zone.c b/lib/dns/zone.c index f4de17f0a0..d97491c61f 100644 --- a/lib/dns/zone.c +++ b/lib/dns/zone.c @@ -323,9 +323,11 @@ struct dns_zone { * module. */ dns_zonestat_level_t statlevel; - bool requeststats_on; + bool requeststats_on; isc_stats_t *requeststats; dns_stats_t *rcvquerystats; + dns_stats_t *dnssecsignstats; + dns_stats_t *dnssecrefreshstats; uint32_t notifydelay; dns_isselffunc_t isself; void *isselfarg; @@ -1022,6 +1024,8 @@ dns_zone_create(dns_zone_t **zonep, isc_mem_t *mctx) { zone->statlevel = dns_zonestat_none; zone->requeststats = NULL; zone->rcvquerystats = NULL; + zone->dnssecsignstats = NULL; + zone->dnssecrefreshstats = NULL; zone->notifydelay = 5; zone->isself = NULL; zone->isselfarg = NULL; @@ -1195,6 +1199,12 @@ zone_free(dns_zone_t *zone) { if (zone->rcvquerystats != NULL){ dns_stats_detach(&zone->rcvquerystats); } + if (zone->dnssecsignstats != NULL){ + dns_stats_detach(&zone->dnssecsignstats); + } + if (zone->dnssecrefreshstats != NULL){ + dns_stats_detach(&zone->dnssecrefreshstats); + } if (zone->db != NULL) { zone_detachdb(zone); } @@ -6511,13 +6521,15 @@ del_sigs(dns_zone_t *zone, dns_db_t *db, dns_dbversion_t *ver, dns_name_t *name, static isc_result_t add_sigs(dns_db_t *db, dns_dbversion_t *ver, dns_name_t *name, - dns_rdatatype_t type, dns_diff_t *diff, dst_key_t **keys, - unsigned int nkeys, isc_mem_t *mctx, isc_stdtime_t inception, - isc_stdtime_t expire, bool check_ksk, - bool keyset_kskonly) + dns_zone_t* zone, dns_rdatatype_t type, dns_diff_t *diff, + dst_key_t **keys, unsigned int nkeys, isc_mem_t *mctx, + isc_stdtime_t inception, isc_stdtime_t expire, + bool check_ksk, bool keyset_kskonly) { isc_result_t result; dns_dbnode_t *node = NULL; + dns_stats_t* dnssecsignstats; + dns_stats_t* dnssecrefreshstats; dns_rdataset_t rdataset; dns_rdata_t sig_rdata = DNS_RDATA_INIT; unsigned char data[1024]; /* XXX */ @@ -6619,12 +6631,27 @@ add_sigs(dns_db_t *db, dns_dbversion_t *ver, dns_name_t *name, CHECK(dns_dnssec_sign(name, &rdataset, keys[i], &inception, &expire, mctx, &buffer, &sig_rdata)); + /* Update the database and journal with the RRSIG. */ /* XXX inefficient - will cause dataset merging */ CHECK(update_one_rr(db, ver, diff, DNS_DIFFOP_ADDRESIGN, name, rdataset.ttl, &sig_rdata)); dns_rdata_reset(&sig_rdata); isc_buffer_init(&buffer, data, sizeof(data)); + + /* Update DNSSEC sign statistics. */ + dnssecsignstats = dns_zone_getdnssecsignstats(zone); + dnssecrefreshstats = dns_zone_getdnssecrefreshstats(zone); + if (dnssecsignstats != NULL) { + dns_dnssecsignstats_increment( + dns_zone_getdnssecsignstats(zone), + dst_key_id(keys[i])); + } + if (dnssecrefreshstats != NULL) { + dns_dnssecsignstats_increment( + dns_zone_getdnssecrefreshstats(zone), + dst_key_id(keys[i])); + } } failure: @@ -6751,9 +6778,9 @@ zone_resigninc(dns_zone_t *zone) { break; } - result = add_sigs(db, version, name, covers, zonediff.diff, - zone_keys, nkeys, zone->mctx, inception, - expire, check_ksk, keyset_kskonly); + result = add_sigs(db, version, name, zone, covers, + zonediff.diff, zone_keys, nkeys, zone->mctx, + inception, expire, check_ksk, keyset_kskonly); if (result != ISC_R_SUCCESS) { dns_zone_log(zone, ISC_LOG_ERROR, "zone_resigninc:add_sigs -> %s", @@ -6809,7 +6836,7 @@ zone_resigninc(dns_zone_t *zone) { * Generate maximum life time signatures so that the above loop * termination is sensible. */ - result = add_sigs(db, version, &zone->origin, dns_rdatatype_soa, + result = add_sigs(db, version, &zone->origin, zone, dns_rdatatype_soa, zonediff.diff, zone_keys, nkeys, zone->mctx, inception, soaexpire, check_ksk, keyset_kskonly); if (result != ISC_R_SUCCESS) { @@ -6998,9 +7025,9 @@ check_if_bottom_of_zone(dns_db_t *db, dns_dbnode_t *node, } static isc_result_t -sign_a_node(dns_db_t *db, dns_name_t *name, dns_dbnode_t *node, - dns_dbversion_t *version, bool build_nsec3, - bool build_nsec, dst_key_t *key, +sign_a_node(dns_db_t *db, dns_zone_t *zone, dns_name_t *name, + dns_dbnode_t *node, dns_dbversion_t *version, + bool build_nsec3, bool build_nsec, dst_key_t *key, isc_stdtime_t inception, isc_stdtime_t expire, unsigned int minimum, bool is_ksk, bool keyset_kskonly, bool is_bottom_of_zone, @@ -7010,6 +7037,9 @@ sign_a_node(dns_db_t *db, dns_name_t *name, dns_dbnode_t *node, dns_rdatasetiter_t *iterator = NULL; dns_rdataset_t rdataset; dns_rdata_t rdata = DNS_RDATA_INIT; + dns_stats_t* dnssecsignstats; + dns_stats_t* dnssecrefreshstats; + isc_buffer_t buffer; unsigned char data[1024]; bool seen_soa, seen_ns, seen_rr, seen_nsec, seen_nsec3, seen_ds; @@ -7111,6 +7141,21 @@ sign_a_node(dns_db_t *db, dns_name_t *name, dns_dbnode_t *node, CHECK(update_one_rr(db, version, diff, DNS_DIFFOP_ADDRESIGN, name, rdataset.ttl, &rdata)); dns_rdata_reset(&rdata); + + /* Update DNSSEC sign statistics. */ + dnssecsignstats = dns_zone_getdnssecsignstats(zone); + dnssecrefreshstats = dns_zone_getdnssecrefreshstats(zone); + if (dnssecsignstats != NULL) { + dns_dnssecsignstats_increment( + dns_zone_getdnssecsignstats(zone), + dst_key_id(key)); + } + if (dnssecrefreshstats != NULL) { + dns_dnssecsignstats_increment( + dns_zone_getdnssecrefreshstats(zone), + dst_key_id(key)); + } + (*signatures)--; next_rdataset: dns_rdataset_disassociate(&rdataset); @@ -7644,7 +7689,7 @@ dns__zone_updatesigs(dns_diff_t *diff, dns_db_t *db, dns_dbversion_t *version, dns_result_totext(result)); return (result); } - result = add_sigs(db, version, &tuple->name, + result = add_sigs(db, version, &tuple->name, zone, tuple->rdata.type, zonediff->diff, zone_keys, nkeys, zone->mctx, inception, exp, check_ksk, keyset_kskonly); @@ -8409,7 +8454,7 @@ zone_nsec3chain(dns_zone_t *zone) { goto failure; } - result = add_sigs(db, version, &zone->origin, dns_rdatatype_soa, + result = add_sigs(db, version, &zone->origin, zone, dns_rdatatype_soa, zonediff.diff, zone_keys, nkeys, zone->mctx, inception, soaexpire, check_ksk, keyset_kskonly); if (result != ISC_R_SUCCESS) { @@ -8971,9 +9016,10 @@ zone_sign(dns_zone_t *zone) { continue; } - CHECK(sign_a_node(db, name, node, version, build_nsec3, - build_nsec, zone_keys[i], inception, - expire, zone->minimum, is_ksk, + CHECK(sign_a_node(db, zone, name, node, version, + build_nsec3, build_nsec, + zone_keys[i], inception, expire, + zone->minimum, is_ksk, (both && keyset_kskonly), is_bottom_of_zone, zonediff.diff, &signatures, zone->mctx)); @@ -9111,7 +9157,7 @@ zone_sign(dns_zone_t *zone) { * Generate maximum life time signatures so that the above loop * termination is sensible. */ - result = add_sigs(db, version, &zone->origin, dns_rdatatype_soa, + result = add_sigs(db, version, &zone->origin, zone, dns_rdatatype_soa, zonediff.diff, zone_keys, nkeys, zone->mctx, inception, soaexpire, check_ksk, keyset_kskonly); if (result != ISC_R_SUCCESS) { @@ -17521,6 +17567,42 @@ dns_zone_setrcvquerystats(dns_zone_t *zone, dns_stats_t *stats) { UNLOCK_ZONE(zone); } +void +dns_zone_setdnssecsignstats(dns_zone_t *zone, dns_stats_t *stats) { + REQUIRE(DNS_ZONE_VALID(zone)); + + LOCK_ZONE(zone); + if (stats != NULL && zone->dnssecsignstats == NULL) { + dns_stats_attach(stats, &zone->dnssecsignstats); + } + UNLOCK_ZONE(zone); +} + +void +dns_zone_setdnssecrefreshstats(dns_zone_t *zone, dns_stats_t *stats) { + REQUIRE(DNS_ZONE_VALID(zone)); + + LOCK_ZONE(zone); + if (stats != NULL && zone->dnssecrefreshstats == NULL) { + dns_stats_attach(stats, &zone->dnssecrefreshstats); + } + UNLOCK_ZONE(zone); +} + +dns_stats_t* +dns_zone_getdnssecsignstats(dns_zone_t *zone) { + REQUIRE(DNS_ZONE_VALID(zone)); + + return (zone->dnssecsignstats); +} + +dns_stats_t* +dns_zone_getdnssecrefreshstats(dns_zone_t *zone) { + REQUIRE(DNS_ZONE_VALID(zone)); + + return (zone->dnssecrefreshstats); +} + isc_stats_t * dns_zone_getrequeststats(dns_zone_t *zone) { /* @@ -18019,8 +18101,7 @@ rr_exists(dns_db_t *db, dns_dbversion_t *ver, dns_name_t *name, */ static isc_result_t add_signing_records(dns_db_t *db, dns_rdatatype_t privatetype, - dns_dbversion_t *ver, dns_diff_t *diff, - bool sign_all) + dns_dbversion_t *ver, dns_diff_t *diff, bool sign_all) { dns_difftuple_t *tuple, *newtuple = NULL; dns_rdata_dnskey_t dnskey; @@ -18149,10 +18230,10 @@ sign_apex(dns_zone_t *zone, dns_db_t *db, dns_dbversion_t *ver, dns_result_totext(result)); goto failure; } - result = add_sigs(db, ver, &zone->origin, dns_rdatatype_dnskey, - zonediff->diff, zone_keys, nkeys, zone->mctx, - inception, keyexpire, check_ksk, - keyset_kskonly); + result = add_sigs(db, ver, &zone->origin, zone, + dns_rdatatype_dnskey, zonediff->diff, + zone_keys, nkeys, zone->mctx, inception, + keyexpire, check_ksk, keyset_kskonly); if (result != ISC_R_SUCCESS) { dnssec_log(zone, ISC_LOG_ERROR, "sign_apex:add_sigs -> %s", @@ -18486,9 +18567,8 @@ zone_rekey(dns_zone_t *zone) { { CHECK(dns_diff_apply(&diff, db, ver)); CHECK(clean_nsec3param(zone, db, ver, &diff)); - CHECK(add_signing_records(db, zone->privatetype, - ver, &diff, - (newalg || fullsign))); + CHECK(add_signing_records(db, zone->privatetype, ver, + &diff, (newalg || fullsign))); CHECK(update_soa_serial(db, ver, &diff, mctx, zone->updatemethod)); CHECK(add_chains(zone, db, ver, &diff)); diff --git a/util/copyrights b/util/copyrights index aa21d47ee8..0161d7c0e0 100644 --- a/util/copyrights +++ b/util/copyrights @@ -1064,6 +1064,7 @@ ./bin/tests/system/statschannel/clean.sh SH 2015,2016,2017,2018,2019 ./bin/tests/system/statschannel/fetch.pl PERL 2015,2016,2018,2019 ./bin/tests/system/statschannel/mem-xml.pl PERL 2017,2018,2019 +./bin/tests/system/statschannel/ns2/sign.sh SH 2019 ./bin/tests/system/statschannel/prereq.sh SH 2015,2016,2018,2019 ./bin/tests/system/statschannel/server-json.pl PERL 2015,2016,2017,2018,2019 ./bin/tests/system/statschannel/server-xml.pl PERL 2015,2016,2017,2018,2019 @@ -1076,6 +1077,8 @@ ./bin/tests/system/statschannel/traffic.expect.4 X 2015,2018,2019 ./bin/tests/system/statschannel/traffic.expect.5 X 2015,2016,2018,2019 ./bin/tests/system/statschannel/traffic.expect.6 X 2015,2016,2018,2019 +./bin/tests/system/statschannel/zones-json.pl PERL 2019 +./bin/tests/system/statschannel/zones-xml.pl PERL 2019 ./bin/tests/system/stop.pl SH 2001,2004,2005,2006,2007,2012,2016,2017,2018,2019 ./bin/tests/system/stop.sh SH 2000,2001,2004,2007,2012,2016,2018,2019 ./bin/tests/system/stopall.sh SH 2018,2019