]> git.ipfire.org Git - thirdparty/unbound.git/commitdiff
- xfr-tsig, TSIG for SOA probe, notify, and on xfr first packet.
authorW.C.A. Wijngaards <wouter@nlnetlabs.nl>
Tue, 19 Aug 2025 13:27:16 +0000 (15:27 +0200)
committerW.C.A. Wijngaards <wouter@nlnetlabs.nl>
Tue, 19 Aug 2025 13:27:16 +0000 (15:27 +0200)
Makefile.in
daemon/worker.c
services/authzone.c
services/authzone.h
util/tsig.c
util/tsig.h

index 60c15f4603a72a932f8affe15ce90b9ece9cbad5..6106380af6c0feeeaaafc7bed109d51715e2e201 100644 (file)
@@ -1364,7 +1364,7 @@ worker.lo worker.o: $(srcdir)/daemon/worker.c config.h $(srcdir)/util/log.h $(sr
  $(srcdir)/services/cache/rrset.h $(srcdir)/services/cache/infra.h $(srcdir)/util/rtt.h \
  $(srcdir)/services/cache/dns.h $(srcdir)/services/authzone.h $(srcdir)/services/mesh.h $(srcdir)/services/rpz.h \
  $(srcdir)/services/localzone.h $(srcdir)/respip/respip.h $(srcdir)/util/data/msgencode.h \
- $(srcdir)/util/data/dname.h $(srcdir)/util/fptr_wlist.h $(srcdir)/util/tube.h $(srcdir)/util/edns.h \
+ $(srcdir)/util/data/dname.h $(srcdir)/util/fptr_wlist.h $(srcdir)/util/tsig.h $(srcdir)/util/tube.h $(srcdir)/util/edns.h \
  $(srcdir)/iterator/iter_fwd.h $(srcdir)/iterator/iter_hints.h $(srcdir)/iterator/iter_utils.h \
  $(srcdir)/iterator/iter_resptype.h $(srcdir)/validator/autotrust.h $(srcdir)/validator/val_anchor.h \
  $(srcdir)/libunbound/context.h $(srcdir)/libunbound/unbound-event.h $(srcdir)/libunbound/libworker.h \
index 1cc094c36b44da07c25cd43fc88f4ad81ed49ab1..c4f0a1931d25f1552f72f7b77a59fbe14678da5b 100644 (file)
@@ -67,6 +67,7 @@
 #include "util/data/dname.h"
 #include "util/fptr_wlist.h"
 #include "util/proxy_protocol.h"
+#include "util/tsig.h"
 #include "util/tube.h"
 #include "util/edns.h"
 #include "util/timeval_func.h"
@@ -1157,16 +1158,22 @@ answer_notify(struct worker* w, struct query_info* qinfo,
        int rcode = LDNS_RCODE_NOERROR;
        uint32_t serial = 0;
        int has_serial;
+       struct tsig_data* tsig = NULL;
+       int tsig_rcode = 0;
        if(!w->env.auth_zones) return;
        has_serial = auth_zone_parse_notify_serial(pkt, &serial);
        if(auth_zones_notify(w->env.auth_zones, &w->env, qinfo->qname,
-               qinfo->qname_len, qinfo->qclass, addr,
-               addrlen, has_serial, serial, &refused)) {
+               qinfo->qname_len, qinfo->qclass, addr, addrlen, has_serial,
+               serial, &refused, pkt, &tsig, &tsig_rcode, w->scratchpad)) {
                rcode = LDNS_RCODE_NOERROR;
        } else {
-               if(refused)
+               if(tsig_rcode != 0) {
+                       rcode = tsig_rcode;
+               } else if(refused) {
                        rcode = LDNS_RCODE_REFUSED;
-               else    rcode = LDNS_RCODE_SERVFAIL;
+               } else {
+                       rcode = LDNS_RCODE_SERVFAIL;
+               }
        }
 
        if(verbosity >= VERB_DETAIL) {
@@ -1184,6 +1191,9 @@ answer_notify(struct worker* w, struct query_info* qinfo,
                else if(rcode == LDNS_RCODE_SERVFAIL)
                        snprintf(buf, sizeof(buf),
                                "servfail for NOTIFY %sfor %s from", sr, zname);
+               else if(rcode != LDNS_RCODE_NOERROR)
+                       snprintf(buf, sizeof(buf),
+                               "error for NOTIFY %sfor %s from", sr, zname);
                else    snprintf(buf, sizeof(buf),
                                "received NOTIFY %sfor %s from", sr, zname);
                log_addr(VERB_DETAIL, buf, addr, addrlen);
@@ -1196,6 +1206,21 @@ answer_notify(struct worker* w, struct query_info* qinfo,
                *(uint16_t*)(void *)sldns_buffer_begin(pkt),
                sldns_buffer_read_u16_at(pkt, 2), edns);
        LDNS_OPCODE_SET(sldns_buffer_begin(pkt), LDNS_PACKET_NOTIFY);
+       if(tsig) {
+               size_t pos = sldns_buffer_limit(pkt);
+               sldns_buffer_clear(pkt);
+               sldns_buffer_set_position(pkt, pos);
+               if(!tsig_sign_reply(tsig, pkt, w->env.tsig_key_table,
+                       (uint64_t)*w->env.now)) {
+                       /* Failed to TSIG sign the reply */
+                       verbose(VERB_ALGO, "Failed to TSIG sign notify reply");
+                       error_encode(pkt, LDNS_RCODE_SERVFAIL, qinfo,
+                               *(uint16_t*)(void *)sldns_buffer_begin(pkt),
+                               sldns_buffer_read_u16_at(pkt, 2), edns);
+                       LDNS_OPCODE_SET(sldns_buffer_begin(pkt), LDNS_PACKET_NOTIFY);
+               }
+               /* The tsig veriable is allocated in the scratch region. */
+       }
 }
 
 static int
index c014c5643c283d0d5b266aeb8627fc09f7c6b0ca..6c9d83444a15dfe262a722dfaa58b955153359bf 100644 (file)
@@ -2336,12 +2336,14 @@ auth_xfer_delete(struct auth_xfer* xfr)
                auth_free_masters(xfr->task_probe->masters);
                comm_point_delete(xfr->task_probe->cp);
                comm_timer_delete(xfr->task_probe->timer);
+               tsig_delete(xfr->task_probe->tsig);
                free(xfr->task_probe);
        }
        if(xfr->task_transfer) {
                auth_free_masters(xfr->task_transfer->masters);
                comm_point_delete(xfr->task_transfer->cp);
                comm_timer_delete(xfr->task_transfer->timer);
+               tsig_delete(xfr->task_transfer->tsig);
                if(xfr->task_transfer->chunks_first) {
                        auth_chunks_delete(xfr->task_transfer);
                }
@@ -3723,11 +3725,30 @@ addr_in_list(struct auth_addr* list, struct sockaddr_storage* addr,
  * addresses in the addr list) */
 static int
 addr_matches_master(struct auth_master* master, struct sockaddr_storage* addr,
-       socklen_t addrlen, struct auth_master** fromhost)
+       socklen_t addrlen, struct auth_master** fromhost,
+       struct tsig_data* tsig)
 {
        struct sockaddr_storage a;
        socklen_t alen = 0;
        int net = 0;
+       if(master->tsig_key_name && master->tsig_key_name[0]) {
+               uint8_t keyname[LDNS_MAX_DOMAINLEN+1];
+               size_t keynamelen = sizeof(keyname);
+               if(!tsig) {
+                       /* This needs a TSIG key, but no TSIG present. */
+                       return 0;
+               }
+               if(sldns_str2wire_dname_buf(master->tsig_key_name, keyname,
+                       &keynamelen) != 0) {
+                       verbose(VERB_ALGO, "could not parse allow-notify-tsig '%s'",
+                               master->tsig_key_name);
+                       return 0;
+               }
+               if(query_dname_compare(keyname, tsig->key_name) != 0) {
+                       /* The TSIG is a different key name, not matched. */
+                       return 0;
+               }
+       }
        if(addr_in_list(master->list, addr, addrlen)) {
                *fromhost = master;
                return 1;       
@@ -3760,11 +3781,12 @@ addr_matches_master(struct auth_master* master, struct sockaddr_storage* addr,
 /** check access list for notifies */
 static int
 az_xfr_allowed_notify(struct auth_xfer* xfr, struct sockaddr_storage* addr,
-       socklen_t addrlen, struct auth_master** fromhost)
+       socklen_t addrlen, struct auth_master** fromhost,
+       struct tsig_data* tsig)
 {
        struct auth_master* p;
        for(p=xfr->allow_notify_list; p; p=p->next) {
-               if(addr_matches_master(p, addr, addrlen, fromhost)) {
+               if(addr_matches_master(p, addr, addrlen, fromhost, tsig)) {
                        return 1;
                }
        }
@@ -3834,7 +3856,8 @@ xfr_process_notify(struct auth_xfer* xfr, struct module_env* env,
 int auth_zones_notify(struct auth_zones* az, struct module_env* env,
        uint8_t* nm, size_t nmlen, uint16_t dclass,
        struct sockaddr_storage* addr, socklen_t addrlen, int has_serial,
-       uint32_t serial, int* refused)
+       uint32_t serial, int* refused, struct sldns_buffer* pkt,
+       struct tsig_data** tsig, int* tsig_rcode, struct regional* scratchpad)
 {
        struct auth_xfer* xfr;
        struct auth_master* fromhost = NULL;
@@ -3849,9 +3872,20 @@ int auth_zones_notify(struct auth_zones* az, struct module_env* env,
        }
        lock_basic_lock(&xfr->lock);
        lock_rw_unlock(&az->lock);
-       
+
+       /* check tsig */
+       if(tsig_in_packet(pkt)) {
+               *tsig_rcode = tsig_parse_verify_query(env->tsig_key_table,
+                       pkt, tsig, scratchpad, (uint64_t)*env->now);
+               if(*tsig_rcode != 0) {
+                       /* The tsig failed to verify. */
+                       lock_basic_unlock(&xfr->lock);
+                       return 0;
+               }
+       }
+
        /* check access list for notifies */
-       if(!az_xfr_allowed_notify(xfr, addr, addrlen, &fromhost)) {
+       if(!az_xfr_allowed_notify(xfr, addr, addrlen, &fromhost, *tsig)) {
                lock_basic_unlock(&xfr->lock);
                /* notify not allowed, refuse the notify */
                *refused = 1;
@@ -4256,6 +4290,37 @@ xfr_create_soa_probe_packet(struct auth_xfer* xfr, sldns_buffer* buf,
        sldns_buffer_write_u16_at(buf, 0, id);
 }
 
+/** sign a query for xfr. */
+static int
+xfr_sign_query(struct tsig_data** tsig, sldns_buffer* pkt,
+       struct module_env* env, char* tsig_key_name)
+{
+       size_t pos;
+       if(*tsig) {
+               tsig_delete(*tsig);
+               *tsig = NULL;
+       }
+       *tsig = tsig_create_fromstr(env->tsig_key_table, tsig_key_name);
+       if(!*tsig) {
+               log_err("tsig key '%s' not found or out of memory",
+                       tsig_key_name);
+               return 0;
+       }
+
+       /* Position the buffer after the packet contents. */
+       pos = sldns_buffer_limit(pkt);
+       sldns_buffer_clear(pkt);
+       sldns_buffer_set_position(pkt, pos);
+       if(!tsig_sign_query(*tsig, pkt, env->tsig_key_table,
+               (uint64_t)*env->now)) {
+               sldns_buffer_flip(pkt);
+               log_err("tsig key '%s': could not sign query", tsig_key_name);
+               return 0;
+       }
+       sldns_buffer_flip(pkt);
+       return 1;
+}
+
 /** create IXFR/AXFR packet for xfr */
 static void
 xfr_create_ixfr_packet(struct auth_xfer* xfr, sldns_buffer* buf, uint16_t id,
@@ -4314,7 +4379,7 @@ xfr_create_ixfr_packet(struct auth_xfer* xfr, sldns_buffer* buf, uint16_t id,
 /** check if returned packet is OK */
 static int
 check_packet_ok(sldns_buffer* pkt, uint16_t qtype, struct auth_xfer* xfr,
-       uint32_t* serial)
+       uint32_t* serial, struct module_env* env)
 {
        /* parse to see if packet worked, valid reply */
 
@@ -4388,6 +4453,20 @@ check_packet_ok(sldns_buffer* pkt, uint16_t qtype, struct auth_xfer* xfr,
                        return 0;
                *serial = sldns_buffer_read_u32(pkt);
        }
+
+       if(xfr->task_probe->tsig) {
+               /* There could be authority or additional RRs in the reply for the
+                * SOA query, if so skip them by tsig_find_rr. */
+               if(!tsig_find_rr(pkt)) {
+                       verbose(VERB_ALGO, "TSIG expected, but not found in reply");
+                       return 0;
+               }
+               if(!tsig_parse_verify_reply(xfr->task_probe->tsig, pkt,
+                       env->tsig_key_table, (uint64_t)*env->now)) {
+                       verbose(VERB_ALGO, "valid TSIG expected in SOA probe reply, but it was not valid");
+                       return 0;
+               }
+       }
        return 1;
 }
 
@@ -5410,6 +5489,9 @@ xfr_transfer_disown(struct auth_xfer* xfr)
        /* remove the commpoint */
        comm_point_delete(xfr->task_transfer->cp);
        xfr->task_transfer->cp = NULL;
+       /* remove the tsig data */
+       tsig_delete(xfr->task_transfer->tsig);
+       xfr->task_transfer->tsig = NULL;
        /* we don't own this item anymore */
        xfr->task_transfer->worker = NULL;
        xfr->task_transfer->env = NULL;
@@ -5572,6 +5654,17 @@ xfr_transfer_init_fetch(struct auth_xfer* xfr, struct module_env* env)
        xfr->task_transfer->id = GET_RANDOM_ID(env->rnd);
        xfr_create_ixfr_packet(xfr, env->scratch_buffer,
                xfr->task_transfer->id, master);
+       if(master->tsig_key_name) {
+               if(!xfr_sign_query(&xfr->task_transfer->tsig,
+                       env->scratch_buffer, env, master->tsig_key_name)) {
+                       char zname[LDNS_MAX_DOMAINLEN], as[256];
+                       dname_str(xfr->name, zname);
+                       addr_port_to_str(&addr, addrlen, as, sizeof(as));
+                       verbose(VERB_ALGO, "failed to TSIG sign xfr "
+                               "for %s to %s", zname, as);
+                       return 0;
+               }
+       }
 
        /* connect on fd */
        xfr->task_transfer->cp = outnet_comm_point_for_tcp(env->outnet,
@@ -5777,7 +5870,7 @@ void auth_xfer_transfer_lookup_callback(void* arg, int rcode, sldns_buffer* buf,
  */
 static int
 check_xfer_packet(sldns_buffer* pkt, struct auth_xfer* xfr,
-       int* gonextonfail, int* transferdone)
+       struct module_env* env, int* gonextonfail, int* transferdone)
 {
        uint8_t* wire = sldns_buffer_begin(pkt);
        int i;
@@ -5802,6 +5895,25 @@ check_xfer_packet(sldns_buffer* pkt, struct auth_xfer* xfr,
                        xfr->task_transfer->master->host);
                return 0;
        }
+       /* check tsig */
+       if(xfr->task_transfer->tsig) {
+               if(xfr->task_transfer->rr_scan_num == 0) {
+                       /* Check TSIG reply on first packet. */
+                       if(!tsig_find_rr(pkt)) {
+                               verbose(VERB_ALGO, "TSIG expected, but not found in reply for xfr to %s",
+                                       xfr->task_transfer->master->host);
+                               return 0;
+                       }
+                       if(!tsig_parse_verify_reply(xfr->task_transfer->tsig,
+                               pkt, env->tsig_key_table,
+                               (uint64_t)*env->now)) {
+                               verbose(VERB_ALGO, "valid TSIG expected in xfr reply to %s, but it was not valid",
+                                       xfr->task_transfer->master->host);
+                               return 0;
+                       }
+               }
+               sldns_buffer_rewind(pkt);
+       }
        if(LDNS_RCODE_WIRE(wire) != LDNS_RCODE_NOERROR) {
                char rcode[32];
                sldns_wire2str_rcode_buf((int)LDNS_RCODE_WIRE(wire), rcode,
@@ -6239,7 +6351,8 @@ auth_xfer_transfer_tcp_callback(struct comm_point* c, void* arg, int err,
        /* handle returned packet */
        /* if it fails, cleanup and end this transfer */
        /* if it needs to fallback from IXFR to AXFR, do that */
-       if(!check_xfer_packet(c->buffer, xfr, &gonextonfail, &transferdone)) {
+       if(!check_xfer_packet(c->buffer, xfr, env, &gonextonfail,
+               &transferdone)) {
                goto failed;
        }
        /* if it is good, link it into the list of data */
@@ -6365,6 +6478,9 @@ xfr_probe_disown(struct auth_xfer* xfr)
        /* remove the commpoint */
        comm_point_delete(xfr->task_probe->cp);
        xfr->task_probe->cp = NULL;
+       /* remove the tsig data */
+       tsig_delete(xfr->task_probe->tsig);
+       xfr->task_probe->tsig = NULL;
        /* we don't own this item anymore */
        xfr->task_probe->worker = NULL;
        xfr->task_probe->env = NULL;
@@ -6422,6 +6538,17 @@ xfr_probe_send_probe(struct auth_xfer* xfr, struct module_env* env,
                xfr->task_probe->id = GET_RANDOM_ID(env->rnd);
        xfr_create_soa_probe_packet(xfr, env->scratch_buffer, 
                xfr->task_probe->id);
+       if(master->tsig_key_name) {
+               if(!xfr_sign_query(&xfr->task_probe->tsig, env->scratch_buffer,
+                       env, master->tsig_key_name)) {
+                       char zname[LDNS_MAX_DOMAINLEN], as[256];
+                       dname_str(xfr->name, zname);
+                       addr_port_to_str(&addr, addrlen, as, sizeof(as));
+                       verbose(VERB_ALGO, "failed to TSIG sign soa probe "
+                               "for %s to %s", zname, as);
+                       return 0;
+               }
+       }
        /* we need to remove the cp if we have a different ip4/ip6 type now */
        if(xfr->task_probe->cp &&
                ((xfr->task_probe->cp_is_ip6 && !addr_is_ip6(&addr, addrlen)) ||
@@ -6541,7 +6668,7 @@ auth_xfer_probe_udp_callback(struct comm_point* c, void* arg, int err,
        if(err == NETEVENT_NOERROR) {
                uint32_t serial = 0;
                if(check_packet_ok(c->buffer, LDNS_RR_TYPE_SOA, xfr,
-                       &serial)) {
+                       &serial, env)) {
                        /* successful lookup */
                        if(verbosity >= VERB_ALGO) {
                                char buf[LDNS_MAX_DOMAINLEN];
@@ -6600,6 +6727,9 @@ auth_xfer_probe_udp_callback(struct comm_point* c, void* arg, int err,
        /* delete commpoint so a new one is created, with a fresh port nr */
        comm_point_delete(xfr->task_probe->cp);
        xfr->task_probe->cp = NULL;
+       /* remove the tsig data */
+       tsig_delete(xfr->task_probe->tsig);
+       xfr->task_probe->tsig = NULL;
 
        /* if the result was not a successful probe, we need
         * to send the next one */
index 7ca0a99635cfefb2e2bf29fb95e668088f138a92..45195586e6af12ab69012d535cdd03b2fdcf909d 100644 (file)
@@ -55,6 +55,7 @@ struct query_info;
 struct dns_msg;
 struct edns_data;
 struct module_env;
+struct tsig_data;
 struct tsig_key_table;
 struct worker;
 struct comm_point;
@@ -358,6 +359,8 @@ struct auth_probe {
        struct comm_timer* timer;
        /** timeout in msec */
        int timeout;
+       /** the tsig data for the packet */
+       struct tsig_data* tsig;
 };
 
 /**
@@ -427,6 +430,8 @@ struct auth_transfer {
        /** timeout for the transfer.
         * on the workers event base. */
        struct comm_timer* timer;
+       /** the tsig data for the transfer */
+       struct tsig_data* tsig;
 };
 
 /** list of addresses */
@@ -620,13 +625,19 @@ int auth_zones_can_fallback(struct auth_zones* az, uint8_t* nm, size_t nmlen,
  * @param has_serial: if true, the notify has a serial attached.
  * @param serial: the serial number, if has_serial is true.
  * @param refused: is set to true on failure to note refused access.
+ * @param pkt: the packet for TSIG verify.
+ * @param tsig: if TSIG, the structure is returned here, allocated in
+ *     the worker scratch region.
+ * @param tsig_rcode: if not NOERROR it is the TSIG error code, TSIG failed.
+ * @param scratchpad: region to allocate tsig in.
  * @return fail on failures (refused is false) and when access is
  *     denied (refused is true).  True when processed.
  */
 int auth_zones_notify(struct auth_zones* az, struct module_env* env,
        uint8_t* nm, size_t nmlen, uint16_t dclass,
        struct sockaddr_storage* addr, socklen_t addrlen, int has_serial,
-       uint32_t serial, int* refused);
+       uint32_t serial, int* refused, struct sldns_buffer* pkt,
+       struct tsig_data** tsig, int* tsig_rcode, struct regional* scratchpad);
 
 /** process notify packet and read serial number from SOA.
  * returns 0 if no soa record in the notify */
index 187c6a3cae2d075c701c5ac7619476f53a692db2..f54051064ef9c9da5122c9334772609f93b2f099 100644 (file)
@@ -1604,6 +1604,51 @@ tsig_find_rr(struct sldns_buffer* pkt)
        return 1;
 }
 
+int tsig_in_packet(struct sldns_buffer* pkt)
+{
+       size_t end_pos, n_rrs;
+       if(sldns_buffer_limit(pkt) < LDNS_HEADER_SIZE) {
+               return 0;
+       }
+       if(LDNS_ARCOUNT(sldns_buffer_begin(pkt)) < 1) {
+               return 0;
+       }
+       n_rrs = LDNS_ANCOUNT(sldns_buffer_begin(pkt))
+             + LDNS_NSCOUNT(sldns_buffer_begin(pkt))
+             + LDNS_ARCOUNT(sldns_buffer_begin(pkt))
+             - 1;
+
+       sldns_buffer_rewind(pkt);
+       sldns_buffer_skip(pkt, LDNS_HEADER_SIZE);
+
+       /* Skip qnames. */
+       if(!skip_pkt_query_rrs(pkt, LDNS_QDCOUNT(sldns_buffer_begin(pkt)))) {
+               return 0;
+       }
+       /* Skip all rrs. */
+       if(!skip_pkt_rrs(pkt, n_rrs)) {
+               return 0;
+       }
+       end_pos = sldns_buffer_position(pkt);
+
+       /* The tsig owner name, the key name */
+       if(sldns_buffer_remaining(pkt) < 1) {
+               return 0;
+       }
+       if(!pkt_dname_len(pkt)) {
+               return 0;
+       }
+       if(sldns_buffer_remaining(pkt) < 2+2+4+2) {
+               return 0;
+       }
+       if(sldns_buffer_read_u16(pkt) != LDNS_RR_TYPE_TSIG) {
+               return 0;
+       }
+
+       sldns_buffer_set_position(pkt, end_pos);
+       return 1;
+}
+
 int
 tsig_sign_reply(struct tsig_data* tsig, struct sldns_buffer* pkt,
        struct tsig_key_table* key_table, uint64_t now)
index 448637a2db5bdbba18f24de90ffc061572de53c3..7278df2f1ad3c5670040e61c3a30f8205c3539ce 100644 (file)
@@ -427,4 +427,13 @@ size_t tsig_reserved_space(struct tsig_data* tsig);
  */
 int tsig_find_rr(struct sldns_buffer* pkt);
 
+/**
+ * See if the packet as a TSIG, or not. Like tsig_find_rr, but it logs
+ * no error for absence of a TSIG.
+ * @param pkt: the packet
+ * @return false if malformed, and false if no tsig. true if tsig,
+ *     and the position is just before the TSIG record. So it can be parsed.
+ */
+int tsig_in_packet(struct sldns_buffer* pkt);
+
 #endif /* UTIL_TSIG_H */