diff --git a/CHANGES b/CHANGES index e50573af38..9863b78bfe 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,6 @@ +3559. [func] Check that both forms of Sender Policy Framework + records exist or do not exist. [RT #33355] + 3558. [bug] IXFR of a DLZ stored zone was broken. [RT #33331] 3557. [bug] Reloading redirect zones was broken. [RT #33292] diff --git a/bin/check/named-checkconf.c b/bin/check/named-checkconf.c index 53e6091fb9..6e832dd6c3 100644 --- a/bin/check/named-checkconf.c +++ b/bin/check/named-checkconf.c @@ -294,6 +294,18 @@ configure_zone(const char *vclass, const char *view, zone_options &= ~DNS_ZONEOPT_CHECKSIBLING; } + obj = NULL; + if (get_maps(maps, "check-spf", &obj)) { + if (strcasecmp(cfg_obj_asstring(obj), "warn") == 0) { + zone_options |= DNS_ZONEOPT_CHECKSPF; + } else if (strcasecmp(cfg_obj_asstring(obj), "ignore") == 0) { + zone_options &= ~DNS_ZONEOPT_CHECKSPF; + } else + INSIST(0); + } else { + zone_options |= DNS_ZONEOPT_CHECKSPF; + } + obj = NULL; if (get_checknames(maps, &obj)) { if (strcasecmp(cfg_obj_asstring(obj), "warn") == 0) { diff --git a/bin/check/named-checkzone.c b/bin/check/named-checkzone.c index 8d978569e3..a13cfa7d5c 100644 --- a/bin/check/named-checkzone.c +++ b/bin/check/named-checkzone.c @@ -155,19 +155,21 @@ main(int argc, char **argv) { if (progmode == progmode_compile) { zone_options |= (DNS_ZONEOPT_CHECKNS | DNS_ZONEOPT_FATALNS | + DNS_ZONEOPT_CHECKSPF | DNS_ZONEOPT_CHECKDUPRR | DNS_ZONEOPT_CHECKNAMES | DNS_ZONEOPT_CHECKNAMESFAIL | DNS_ZONEOPT_CHECKWILDCARD); } else - zone_options |= DNS_ZONEOPT_CHECKDUPRR; + zone_options |= (DNS_ZONEOPT_CHECKDUPRR | + DNS_ZONEOPT_CHECKSPF); #define ARGCMP(X) (strcmp(isc_commandline_argument, X) == 0) isc_commandline_errprint = ISC_FALSE; while ((c = isc_commandline_parse(argc, argv, - "c:df:hi:jJ:k:L:m:n:qr:s:t:o:vw:DF:M:S:W:")) + "c:df:hi:jJ:k:L:m:n:qr:s:t:o:vw:DF:M:S:T:W:")) != EOF) { switch (c) { case 'c': @@ -389,6 +391,18 @@ main(int argc, char **argv) { } break; + case 'T': + if (ARGCMP("warn")) { + zone_options |= DNS_ZONEOPT_CHECKSPF; + } else if (ARGCMP("ignore")) { + zone_options &= ~DNS_ZONEOPT_CHECKSPF; + } else { + fprintf(stderr, "invalid argument to -T: %s\n", + isc_commandline_argument); + exit(1); + } + break; + case 'W': if (ARGCMP("warn")) zone_options |= DNS_ZONEOPT_CHECKWILDCARD; diff --git a/bin/check/named-checkzone.docbook b/bin/check/named-checkzone.docbook index 934993dfce..98d3fc3ff9 100644 --- a/bin/check/named-checkzone.docbook +++ b/bin/check/named-checkzone.docbook @@ -80,6 +80,7 @@ + @@ -105,6 +106,7 @@ + @@ -419,6 +421,18 @@ + + -T mode + + + Check if Sender Policy Framework records (TXT and SPF) + both exist or both don't exist. A warning is issued + if they don't match. Possible modes are + "warn" (default), "ignore". + + + + -w directory diff --git a/bin/named/config.c b/bin/named/config.c index 2de1db3786..93cbd460f6 100644 --- a/bin/named/config.c +++ b/bin/named/config.c @@ -151,6 +151,7 @@ options {\n\ check-names response ignore;\n\ check-dup-records warn;\n\ check-mx warn;\n\ + check-spf warn;\n\ acache-enable no;\n\ acache-cleaning-interval 60;\n\ max-acache-size 16M;\n\ diff --git a/bin/named/zoneconf.c b/bin/named/zoneconf.c index cffe4c5c71..47565c7a97 100644 --- a/bin/named/zoneconf.c +++ b/bin/named/zoneconf.c @@ -1259,6 +1259,17 @@ ns_zone_configure(const cfg_obj_t *config, const cfg_obj_t *vconfig, dns_zone_setoption(zone, DNS_ZONEOPT_CHECKSIBLING, cfg_obj_asboolean(obj)); + obj = NULL; + result = ns_config_get(maps, "check-spf", &obj); + INSIST(result == ISC_R_SUCCESS && obj != NULL); + if (strcasecmp(cfg_obj_asstring(obj), "warn") == 0) { + check = ISC_TRUE; + } else if (strcasecmp(cfg_obj_asstring(obj), "ignore") == 0) { + check = ISC_FALSE; + } else + INSIST(0); + dns_zone_setoption(zone, DNS_ZONEOPT_CHECKSPF, check); + obj = NULL; result = ns_config_get(maps, "zero-no-soa-ttl", &obj); INSIST(result == ISC_R_SUCCESS && obj != NULL); diff --git a/bin/tests/system/checkzone/tests.sh b/bin/tests/system/checkzone/tests.sh index 59643b12cf..a727b411c8 100644 --- a/bin/tests/system/checkzone/tests.sh +++ b/bin/tests/system/checkzone/tests.sh @@ -50,6 +50,21 @@ cmp -s test.changed.db test.out1.db || ret=1 mv -f test.orig.db.jnl test.journal $CHECKZONE -D -J test.journal -o test.out2.db test test.orig.db > /dev/null 2>&1 || ret=1 cmp -s test.changed.db test.out2.db || ret=1 +n=`expr $n + 1` +if [ $ret != 0 ]; then echo "I:failed"; fi +status=`expr $status + $ret` + +echo "I:checking with spf warnings ($n)" +ret=0 +$CHECKZONE example zones/spf.db > test.out1.$n 2>&1 || ret=1 +$CHECKZONE -T ignore example zones/spf.db > test.out2.$n 2>&1 || ret=1 +grep "'x.example' found SPF/TXT" test.out1.$n > /dev/null || ret=1 +grep "'y.example' found SPF/SPF" test.out1.$n > /dev/null || ret=1 +grep "'example' found SPF/" test.out1.$n > /dev/null && ret=1 +grep "'x.example' found SPF/" test.out2.$n > /dev/null && ret=1 +grep "'y.example' found SPF/" test.out2.$n > /dev/null && ret=1 +grep "'example' found SPF/" test.out2.$n > /dev/null && ret=1 +n=`expr $n + 1` if [ $ret != 0 ]; then echo "I:failed"; fi status=`expr $status + $ret` diff --git a/bin/tests/system/checkzone/zones/spf.db b/bin/tests/system/checkzone/zones/spf.db new file mode 100644 index 0000000000..ffa850ad79 --- /dev/null +++ b/bin/tests/system/checkzone/zones/spf.db @@ -0,0 +1,21 @@ +; Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC") +; +; Permission to use, copy, modify, and/or distribute this software for any +; purpose with or without fee is hereby granted, provided that the above +; copyright notice and this permission notice appear in all copies. +; +; THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +; REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +; AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +; INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +; LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +; OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +; PERFORMANCE OF THIS SOFTWARE. + +@ 0 IN SOA . . 0 0 0 0 0 +@ 0 IN NS . +@ 0 IN TXT "v=spf1 -all" +@ 0 IN SPF "v=spf1 -all" +x 0 IN TXT "v=spf1" +y 0 IN SPF "v=spf1" +y 0 IN TXT "a non spf record" diff --git a/bin/tests/system/conf.sh.in b/bin/tests/system/conf.sh.in index e505d74d7f..3093512d60 100644 --- a/bin/tests/system/conf.sh.in +++ b/bin/tests/system/conf.sh.in @@ -64,7 +64,7 @@ SUBDIRS="acl additional allow_query addzone autosign builtin dsdigest dscp ecdsa formerr forward glue gost ixfr inline limits logfileconfig lwresd masterfile masterformat metadata notify nsupdate pending pkcs11 redirect resolver rndc rpz - rrl rrsetorder rsabigexponent sortlist smartsign staticstub + rrl rrsetorder rsabigexponent smartsign sortlist spf staticstub statistics stub tkey tsig tsiggss unknown upforwd verify views wildcard xfer xferquota zonechecks" diff --git a/bin/tests/system/spf/clean.sh b/bin/tests/system/spf/clean.sh new file mode 100644 index 0000000000..9c95122b66 --- /dev/null +++ b/bin/tests/system/spf/clean.sh @@ -0,0 +1,2 @@ +rm -f ns1/named.run +rm -f ns1/named.memstats diff --git a/bin/tests/system/spf/ns1/named.conf b/bin/tests/system/spf/ns1/named.conf new file mode 100644 index 0000000000..7d5dcfb041 --- /dev/null +++ b/bin/tests/system/spf/ns1/named.conf @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC") + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE + * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +controls { /* empty */ }; + +options { + query-source address 10.53.0.1; + notify-source 10.53.0.1; + transfer-source 10.53.0.1; + port 5300; + pid-file "named.pid"; + listen-on { 10.53.0.1; }; + listen-on-v6 { none; }; + recursion no; + notify yes; + ixfr-from-differences yes; +}; + +zone "spf" { + type master; + file "spf.db"; +}; + +zone "warn" { + type master; + file "spf.db"; + check-spf warn; +}; + +zone "nowarn" { + type master; + file "spf.db"; + check-spf ignore; +}; diff --git a/bin/tests/system/spf/ns1/spf.db b/bin/tests/system/spf/ns1/spf.db new file mode 100644 index 0000000000..ffa850ad79 --- /dev/null +++ b/bin/tests/system/spf/ns1/spf.db @@ -0,0 +1,21 @@ +; Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC") +; +; Permission to use, copy, modify, and/or distribute this software for any +; purpose with or without fee is hereby granted, provided that the above +; copyright notice and this permission notice appear in all copies. +; +; THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +; REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +; AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +; INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +; LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +; OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +; PERFORMANCE OF THIS SOFTWARE. + +@ 0 IN SOA . . 0 0 0 0 0 +@ 0 IN NS . +@ 0 IN TXT "v=spf1 -all" +@ 0 IN SPF "v=spf1 -all" +x 0 IN TXT "v=spf1" +y 0 IN SPF "v=spf1" +y 0 IN TXT "a non spf record" diff --git a/bin/tests/system/spf/tests.sh b/bin/tests/system/spf/tests.sh new file mode 100644 index 0000000000..6acd2836a3 --- /dev/null +++ b/bin/tests/system/spf/tests.sh @@ -0,0 +1,45 @@ +#!/bin/sh +# +# Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC") +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. + +SYSTEMTESTTOP=.. +. $SYSTEMTESTTOP/conf.sh + +n=1 +status=0 + +echo "I:checking that SPF warnings have been correctly generated ($n)" +ret=0 + +grep "zone spf/IN: loaded serial 0" ns1/named.run > /dev/null || ret=1 +grep "'x.spf' found SPF/TXT" ns1/named.run > /dev/null || ret=1 +grep "'y.spf' found SPF/SPF" ns1/named.run > /dev/null || ret=1 +grep "'spf' found SPF/" ns1/named.run > /dev/null && ret=1 + +grep "zone warn/IN: loaded serial 0" ns1/named.run > /dev/null || ret=1 +grep "'x.warn' found SPF/TXT" ns1/named.run > /dev/null || ret=1 +grep "'y.warn' found SPF/SPF" ns1/named.run > /dev/null || ret=1 +grep "'warn' found SPF/" ns1/named.run > /dev/null && ret=1 + +grep "zone nowarn/IN: loaded serial 0" ns1/named.run > /dev/null || ret=1 +grep "'x.nowarn' found SPF/" ns1/named.run > /dev/null && ret=1 +grep "'y.nowarn' found SPF/" ns1/named.run > /dev/null && ret=1 +grep "'nowarn' found SPF/" ns1/named.run > /dev/null && ret=1 +n=`expr $n + 1` +if [ $ret != 0 ]; then echo "I:failed"; fi +status=`expr $status + $ret` + +echo "I:exit status: $status" +exit $status diff --git a/doc/arm/Bv9ARM-book.xml b/doc/arm/Bv9ARM-book.xml index c8fc5cfbc5..2df02e586d 100644 --- a/doc/arm/Bv9ARM-book.xml +++ b/doc/arm/Bv9ARM-book.xml @@ -5343,6 +5343,7 @@ badresp:1,adberr:0,findfail:0,valfail:0] check-mx-cname ( warn | fail | ignore ); check-srv-cname ( warn | fail | ignore ); check-sibling yes_or_no; + check-spf ( warn | fail | ignore ); allow-new-zones { yes_or_no }; allow-notify { address_match_list }; allow-query { address_match_list }; @@ -7212,6 +7213,12 @@ options { checks use named-checkzone). The default is yes. + + Check that the two forms of Sender Policy Framework + records (TXT and SPF) either both exist or both + don't exist. Warnings are emitted it they don't + and be suppressed with check-spf. + @@ -7247,6 +7254,19 @@ options { + + check-spf + + + When performing integrity checks, check that the + two forms of Sender Policy Framwork records (TXT + and SPF) both exist or both don't exist and issue + a warning if not met. The default is + warn. + + + + zero-no-soa-ttl @@ -10982,6 +11002,7 @@ view "external" { check-names (warn|fail|ignore) ; check-mx (warn|fail|ignore) ; check-wildcard yes_or_no; + check-spf ( warn | fail | ignore ); check-integrity yes_or_no ; dialup dialup_option ; file string ; @@ -11631,6 +11652,16 @@ zone zone_name class + + check-spf + + + See the description of + check-spf in . + + + + check-wildcard diff --git a/lib/dns/include/dns/zone.h b/lib/dns/include/dns/zone.h index cedd63e052..1d1c6b3cae 100644 --- a/lib/dns/include/dns/zone.h +++ b/lib/dns/include/dns/zone.h @@ -87,6 +87,7 @@ typedef enum { #define DNS_ZONEOPT_DNSKEYKSKONLY 0x10000000U /*%< dnssec-dnskey-kskonly */ #define DNS_ZONEOPT_CHECKDUPRR 0x20000000U /*%< check-dup-records */ #define DNS_ZONEOPT_CHECKDUPRRFAIL 0x40000000U /*%< fatal check-dup-records failures */ +#define DNS_ZONEOPT_CHECKSPF 0x80000000U /*%< check SPF records */ #ifndef NOMINUM_PUBLIC /* diff --git a/lib/dns/zone.c b/lib/dns/zone.c index 15f573a708..592355a2b8 100644 --- a/lib/dns/zone.c +++ b/lib/dns/zone.c @@ -2610,6 +2610,30 @@ zone_check_dup(dns_zone_t *zone, dns_db_t *db) { return (ok); } +static isc_boolean_t +isspf(const dns_rdata_t *rdata) { + char buf[1024]; + const unsigned char *data = rdata->data; + unsigned int rdl = rdata->length, i = 0, tl, len; + + while (rdl > 0U) { + len = tl = *data; + ++data; + --rdl; + INSIST(tl <= rdl); + if (len > sizeof(buf) - i - 1) + len = sizeof(buf) - i - 1; + memcpy(buf + i, data, len); + i += len; + data += tl; + rdl -= tl; + } + buf[i] = 0; + if (strncmp(buf, "v=spf1", 6) == 0 && (buf[6] == 0 || buf[6] == ' ')) + return (ISC_TRUE); + return (ISC_FALSE); +} + static isc_boolean_t integrity_checks(dns_zone_t *zone, dns_db_t *db) { dns_dbiterator_t *dbiterator = NULL; @@ -2624,7 +2648,7 @@ integrity_checks(dns_zone_t *zone, dns_db_t *db) { dns_name_t *name; dns_name_t *bottom; isc_result_t result; - isc_boolean_t ok = ISC_TRUE; + isc_boolean_t ok = ISC_TRUE, have_spf, have_txt; dns_fixedname_init(&fixed); name = dns_fixedname_name(&fixed); @@ -2702,7 +2726,7 @@ integrity_checks(dns_zone_t *zone, dns_db_t *db) { result = dns_db_findrdataset(db, node, NULL, dns_rdatatype_srv, 0, 0, &rdataset, NULL); if (result != ISC_R_SUCCESS) - goto next; + goto checkspf; result = dns_rdataset_first(&rdataset); while (result == ISC_R_SUCCESS) { dns_rdataset_current(&rdataset, &rdata); @@ -2715,6 +2739,50 @@ integrity_checks(dns_zone_t *zone, dns_db_t *db) { } dns_rdataset_disassociate(&rdataset); + checkspf: + /* + * Check if there is a type TXT spf record without a type SPF + * RRset being present. + */ + if (!DNS_ZONE_OPTION(zone, DNS_ZONEOPT_CHECKSPF)) + goto next; + if (zone->rdclass != dns_rdataclass_in) + goto next; + have_spf = have_txt = ISC_FALSE; + result = dns_db_findrdataset(db, node, NULL, dns_rdatatype_spf, + 0, 0, &rdataset, NULL); + if (result == ISC_R_SUCCESS) { + dns_rdataset_disassociate(&rdataset); + have_spf = ISC_TRUE; + } + result = dns_db_findrdataset(db, node, NULL, dns_rdatatype_txt, + 0, 0, &rdataset, NULL); + if (result != ISC_R_SUCCESS) + goto notxt; + result = dns_rdataset_first(&rdataset); + while (result == ISC_R_SUCCESS) { + dns_rdataset_current(&rdataset, &rdata); + have_txt = isspf(&rdata); + dns_rdata_reset(&rdata); + if (have_txt) + break; + result = dns_rdataset_next(&rdataset); + } + dns_rdataset_disassociate(&rdataset); + + notxt: + if (have_spf != have_txt) { + char namebuf[DNS_NAME_FORMATSIZE]; + const char *found = have_txt ? "TXT" : "SPF"; + const char *need = have_txt ? "SPF" : "TXT"; + + dns_name_format(name, namebuf, sizeof(namebuf)); + dns_zone_log(zone, ISC_LOG_WARNING, "'%s' found SPF/%s " + "record but no SPF/%s record found, add " + "matching type %s record", namebuf, found, + need, need); + } + next: dns_db_detachnode(db, &node); result = dns_dbiterator_next(dbiterator); diff --git a/lib/isccfg/namedconf.c b/lib/isccfg/namedconf.c index c65727f341..b2c2946ca5 100644 --- a/lib/isccfg/namedconf.c +++ b/lib/isccfg/namedconf.c @@ -549,6 +549,12 @@ static cfg_type_t cfg_type_checkmode = { &cfg_rep_string, &checkmode_enums }; +static const char *warn_enums[] = { "warn", "ignore", NULL }; +static cfg_type_t cfg_type_warn = { + "warn", cfg_parse_enum, cfg_print_ustring, cfg_doc_enum, + &cfg_rep_string, &warn_enums +}; + static cfg_tuplefielddef_t checknames_fields[] = { { "type", &cfg_type_checktype, 0 }, { "mode", &cfg_type_checkmode, 0 }, @@ -1588,6 +1594,7 @@ zone_clauses[] = { { "check-mx", &cfg_type_checkmode, 0 }, { "check-mx-cname", &cfg_type_checkmode, 0 }, { "check-sibling", &cfg_type_boolean, 0 }, + { "check-spf", &cfg_type_warn, 0 }, { "check-srv-cname", &cfg_type_checkmode, 0 }, { "check-wildcard", &cfg_type_boolean, 0 }, { "dialup", &cfg_type_dialuptype, 0 },