From: W.C.A. Wijngaards Date: Tue, 19 Aug 2025 13:27:16 +0000 (+0200) Subject: - xfr-tsig, TSIG for SOA probe, notify, and on xfr first packet. X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=888d5ce9f936310d57212ad177120b13ff7796b0;p=thirdparty%2Funbound.git - xfr-tsig, TSIG for SOA probe, notify, and on xfr first packet. --- diff --git a/Makefile.in b/Makefile.in index 60c15f460..6106380af 100644 --- a/Makefile.in +++ b/Makefile.in @@ -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 \ diff --git a/daemon/worker.c b/daemon/worker.c index 1cc094c36..c4f0a1931 100644 --- a/daemon/worker.c +++ b/daemon/worker.c @@ -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 diff --git a/services/authzone.c b/services/authzone.c index c014c5643..6c9d83444 100644 --- a/services/authzone.c +++ b/services/authzone.c @@ -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 */ diff --git a/services/authzone.h b/services/authzone.h index 7ca0a9963..45195586e 100644 --- a/services/authzone.h +++ b/services/authzone.h @@ -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 */ diff --git a/util/tsig.c b/util/tsig.c index 187c6a3ca..f54051064 100644 --- a/util/tsig.c +++ b/util/tsig.c @@ -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) diff --git a/util/tsig.h b/util/tsig.h index 448637a2d..7278df2f1 100644 --- a/util/tsig.h +++ b/util/tsig.h @@ -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 */