Commit 1c3c13ca68 for bind

commit 1c3c13ca6830a17625e3bde40ededeb890dc856b
Author: Alessio Podda <alessio@isc.org>
Date:   Tue Sep 15 00:47:15 2026 +0200

    Compact embedded zone source addresses

    The ISC address structures have embedded intrusive linked lists that
    are unused. Also, in the zone, the address family is already known.
    We can reduce memory consumption by removing the embedded linked
    lists and specializing to the address family.

diff --git a/lib/dns/include/dns/notify.h b/lib/dns/include/dns/notify.h
index 37aaa7c488..96fe7d20c8 100644
--- a/lib/dns/include/dns/notify.h
+++ b/lib/dns/include/dns/notify.h
@@ -19,6 +19,7 @@

 #include <dns/name.h>
 #include <dns/types.h>
+#include <dns/zoneaddr.h>

 #define NOTIFY_MAGIC		 ISC_MAGIC('N', 't', 'f', 'y')
 #define DNS_NOTIFY_VALID(notify) ISC_MAGIC_VALID(notify, NOTIFY_MAGIC)
@@ -37,8 +38,8 @@ struct dns_notifyctx {
 	dns_notifytype_t notifytype;
 	uint32_t	 notifydefer;
 	uint32_t	 notifydelay;
-	isc_sockaddr_t	 notifysrc4;
-	isc_sockaddr_t	 notifysrc6;
+	zone_addr4_t	 notifysrc4;
+	zone_addr6_t	 notifysrc6;
 };

 /*%
diff --git a/lib/dns/include/dns/zoneaddr.h b/lib/dns/include/dns/zoneaddr.h
new file mode 100644
index 0000000000..01aeb973a7
--- /dev/null
+++ b/lib/dns/include/dns/zoneaddr.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ *
+ * 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 https://mozilla.org/MPL/2.0/.
+ *
+ * See the COPYRIGHT file distributed with this work for additional
+ * information regarding copyright ownership.
+ */
+
+#pragma once
+
+#include <stdint.h>
+
+#include <isc/net.h>
+
+/*
+ * Embedded endpoints need neither socket-address list links nor a length.
+ * Configured source addresses have port zero and no IPv6 flow information.
+ * Preserve the IPv6 scope ID for interface-scoped addresses.
+ * The all-zero IPv4/IPv6 representations are wildcard addresses.
+ */
+typedef struct {
+	struct in_addr address;
+} zone_addr4_t;
+
+typedef struct {
+	struct in6_addr address;
+	uint32_t	scope;
+} zone_addr6_t;
+
+typedef struct {
+	union {
+		zone_addr4_t in;
+		zone_addr6_t in6;
+	} type;
+	sa_family_t family;
+} zone_addr_t;
diff --git a/lib/dns/include/dns/zoneproperties.h b/lib/dns/include/dns/zoneproperties.h
index 444ac1ad92..c9c4c3ff17 100644
--- a/lib/dns/include/dns/zoneproperties.h
+++ b/lib/dns/include/dns/zoneproperties.h
@@ -397,6 +397,7 @@ dns_zone_setxfrsource4(dns_zone_t *zone, const isc_sockaddr_t *xfrsource);
  * Require:
  *\li	'zone' to be a valid zone.
  *\li	'xfrsource' to contain the address.
+ *\li	The source port must be zero.
  */

 void
@@ -418,6 +419,7 @@ dns_zone_setxfrsource6(dns_zone_t *zone, const isc_sockaddr_t *xfrsource);
  * Require:
  *\li	'zone' to be a valid zone.
  *\li	'xfrsource' to contain the address.
+ *\li	The source port and IPv6 flow information must be zero.
  */

 void
@@ -439,6 +441,7 @@ dns_zone_setparentalsrc4(dns_zone_t *zone, const isc_sockaddr_t *parentalsrc);
  * Require:
  *\li	'zone' to be a valid zone.
  *\li	'parentalsrc' to contain the address.
+ *\li	The source port must be zero.
  */

 void
@@ -460,6 +463,7 @@ dns_zone_setparentalsrc6(dns_zone_t *zone, const isc_sockaddr_t *parentalsrc);
  * Require:
  *\li	'zone' to be a valid zone.
  *\li	'parentalsrc' to contain the address.
+ *\li	The source port and IPv6 flow information must be zero.
  */

 void
@@ -483,6 +487,7 @@ dns_zone_setnotifysrc4(dns_zone_t *zone, dns_rdatatype_t type,
  *\li	'zone' to be a valid zone.
  *\li	'type' to be a valid notify RRtype.
  *\li	'notifysrc' to contain the address.
+ *\li	The source port must be zero.
  */

 void
@@ -495,6 +500,7 @@ dns_zone_setnotifysrc6(dns_zone_t *zone, dns_rdatatype_t type,
  *\li	'zone' to be a valid zone.
  *\li	'type' to be a valid notify RRtype.
  *\li	'notifysrc' to contain the address.
+ *\li	The source port and IPv6 flow information must be zero.
  */

 void
diff --git a/lib/dns/notify.c b/lib/dns/notify.c
index 662092ae2e..983ef68b5a 100644
--- a/lib/dns/notify.c
+++ b/lib/dns/notify.c
@@ -48,8 +48,6 @@ dns_notifyctx_init(dns_notifyctx_t *nctx, dns_rdatatype_t type) {
 		.notifydelay = 5,
 		.notifies = ISC_LIST_INITIALIZER,
 	};
-	isc_sockaddr_any(&ctx.notifysrc4);
-	isc_sockaddr_any6(&ctx.notifysrc6);

 	*nctx = ctx;
 }
@@ -427,7 +425,8 @@ notify_send_toaddr(void *arg) {

 			src = notify->src;
 			if (isc_sockaddr_equal(&src, &any)) {
-				src = notifyctx->notifysrc4;
+				src = zone_addr4_tosockaddr(
+					&notifyctx->notifysrc4);
 			}
 		}
 		break;
@@ -438,7 +437,8 @@ notify_send_toaddr(void *arg) {

 			src = notify->src;
 			if (isc_sockaddr_equal(&src, &any)) {
-				src = notifyctx->notifysrc6;
+				src = zone_addr6_tosockaddr(
+					&notifyctx->notifysrc6);
 			}
 		}
 		break;
@@ -624,11 +624,11 @@ notify_isself(dns_notify_t *notify, isc_sockaddr_t *dst) {

 	switch (isc_sockaddr_pf(dst)) {
 	case PF_INET:
-		src = notifyctx->notifysrc4;
+		src = zone_addr4_tosockaddr(&notifyctx->notifysrc4);
 		isc_sockaddr_any(&any);
 		break;
 	case PF_INET6:
-		src = notifyctx->notifysrc6;
+		src = zone_addr6_tosockaddr(&notifyctx->notifysrc6);
 		isc_sockaddr_any6(&any);
 		break;
 	default:
diff --git a/lib/dns/zone.c b/lib/dns/zone.c
index 72e854528e..d86a9a0ee5 100644
--- a/lib/dns/zone.c
+++ b/lib/dns/zone.c
@@ -526,10 +526,6 @@ dns_zone_create(dns_zone_t **zonep, isc_mem_t *mctx, isc_tid_t tid) {
 	isc_refcount_init(&zone->references, 1);
 	isc_refcount_init(&zone->irefs, 0);
 	dns_name_init(&zone->origin);
-	isc_sockaddr_any(&zone->parentalsrc4);
-	isc_sockaddr_any6(&zone->parentalsrc6);
-	isc_sockaddr_any(&zone->xfrsource4);
-	isc_sockaddr_any6(&zone->xfrsource6);

 	zone->primaries = r;
 	zone->parentals = r;
@@ -11306,7 +11302,7 @@ stub_glue_response(void *arg) {
 	char source[ISC_SOCKADDR_FORMATSIZE];
 	uint32_t addr_count, cnamecnt;
 	isc_result_t result;
-	isc_sockaddr_t curraddr;
+	isc_sockaddr_t curraddr, sourceaddr;
 	dns_rdataset_t *addr_rdataset = NULL;
 	dns_dbnode_t *node = NULL;

@@ -11323,13 +11319,14 @@ stub_glue_response(void *arg) {
 		goto cleanup;
 	}

+	sourceaddr = zone_addr_tosockaddr(&zone->sourceaddr);
 	curraddr = dns_remote_curraddr(&zone->primaries);
 	isc_sockaddr_format(&curraddr, primary, sizeof(primary));
-	isc_sockaddr_format(&zone->sourceaddr, source, sizeof(source));
+	isc_sockaddr_format(&sourceaddr, source, sizeof(source));

 	if (dns_request_getresult(request) != ISC_R_SUCCESS) {
 		dns_unreachcache_add(zone->view->unreachcache, &curraddr,
-				     &zone->sourceaddr);
+				     &sourceaddr);
 		dns_zone_log(zone, ISC_LOG_INFO,
 			     "could not refresh stub from primary %s"
 			     " (source %s): %s",
@@ -11505,7 +11502,7 @@ stub_request_nameserver_address(struct stub_cb_args *args, bool ipv4,
 	dns_zone_t *zone;
 	isc_result_t result;
 	struct stub_glue_request *sgr;
-	isc_sockaddr_t curraddr;
+	isc_sockaddr_t curraddr, sourceaddr;

 	zone = args->stub->zone;
 	sgr = isc_mem_get(zone->mctx, sizeof(*sgr));
@@ -11532,13 +11529,13 @@ stub_request_nameserver_address(struct stub_cb_args *args, bool ipv4,

 	atomic_fetch_add_release(&args->stub->pending_requests, 1);

+	sourceaddr = zone_addr_tosockaddr(&zone->sourceaddr);
 	curraddr = dns_remote_curraddr(&zone->primaries);
 	result = dns_request_create(
-		zone->view->requestmgr, message, &zone->sourceaddr, &curraddr,
-		NULL, NULL, DNS_REQUESTOPT_TCP, args->tsig_key,
-		args->connect_timeout, args->timeout, UDP_REQUEST_TIMEOUT,
-		UDP_REQUEST_RETRIES, zone->loop, stub_glue_response, sgr,
-		&sgr->request);
+		zone->view->requestmgr, message, &sourceaddr, &curraddr, NULL,
+		NULL, DNS_REQUESTOPT_TCP, args->tsig_key, args->connect_timeout,
+		args->timeout, UDP_REQUEST_TIMEOUT, UDP_REQUEST_RETRIES,
+		zone->loop, stub_glue_response, sgr, &sgr->request);

 	if (result != ISC_R_SUCCESS) {
 		uint_fast32_t pr;
@@ -11717,7 +11714,7 @@ stub_callback(void *arg) {
 	char source[ISC_SOCKADDR_FORMATSIZE];
 	uint32_t nscnt, cnamecnt;
 	isc_result_t result;
-	isc_sockaddr_t curraddr;
+	isc_sockaddr_t curraddr, sourceaddr;
 	isc_time_t now;
 	bool exiting = false;

@@ -11735,9 +11732,10 @@ stub_callback(void *arg) {
 		goto exiting;
 	}

+	sourceaddr = zone_addr_tosockaddr(&zone->sourceaddr);
 	curraddr = dns_remote_curraddr(&zone->primaries);
 	isc_sockaddr_format(&curraddr, primary, sizeof(primary));
-	isc_sockaddr_format(&zone->sourceaddr, source, sizeof(source));
+	isc_sockaddr_format(&sourceaddr, source, sizeof(source));

 	result = dns_request_getresult(request);
 	switch (result) {
@@ -11758,7 +11756,7 @@ stub_callback(void *arg) {
 		FALLTHROUGH;
 	default:
 		dns_unreachcache_add(zone->view->unreachcache, &curraddr,
-				     &zone->sourceaddr);
+				     &sourceaddr);
 		dns_zone_log(zone, ISC_LOG_INFO,
 			     "could not refresh stub from primary "
 			     "%s (source %s): %s",
@@ -12056,7 +12054,7 @@ refresh_callback(void *arg) {
 	dns_rdata_soa_t soa;
 	isc_result_t result;
 	const isc_result_t eresult = dns_request_getresult(request);
-	isc_sockaddr_t curraddr;
+	isc_sockaddr_t curraddr, sourceaddr;
 	uint32_t serial, oldserial = 0;
 	bool do_queue_xfrin = false;

@@ -12081,9 +12079,10 @@ refresh_callback(void *arg) {
 	/*
 	 * If timeout, log and try the next primary
 	 */
+	sourceaddr = zone_addr_tosockaddr(&zone->sourceaddr);
 	curraddr = dns_remote_curraddr(&zone->primaries);
 	isc_sockaddr_format(&curraddr, primary, sizeof(primary));
-	isc_sockaddr_format(&zone->sourceaddr, source, sizeof(source));
+	isc_sockaddr_format(&sourceaddr, source, sizeof(source));

 	switch (eresult) {
 	case ISC_R_SUCCESS:
@@ -12114,7 +12113,7 @@ refresh_callback(void *arg) {
 			{
 				if (dns_unreachcache_find(
 					    zone->view->unreachcache, &curraddr,
-					    &zone->sourceaddr) != ISC_R_SUCCESS)
+					    &sourceaddr) != ISC_R_SUCCESS)
 				{
 					DNS_ZONE_SETFLAG(
 						zone,
@@ -12357,7 +12356,7 @@ refresh_callback(void *arg) {
 	    isc_serial_gt(serial, oldserial))
 	{
 		if (dns_unreachcache_find(zone->view->unreachcache, &curraddr,
-					  &zone->sourceaddr) == ISC_R_SUCCESS)
+					  &sourceaddr) == ISC_R_SUCCESS)
 		{
 			dns_zone_logc(zone, DNS_LOGCATEGORY_XFER_IN,
 				      ISC_LOG_INFO,
@@ -12539,7 +12538,7 @@ soa_query(void *arg) {
 	bool cancel = true;
 	bool have_xfrsource = false, reqnsid, reqexpire;
 	uint16_t udpsize = SEND_BUFFER_SIZE;
-	isc_sockaddr_t curraddr, sourceaddr;
+	isc_sockaddr_t curraddr, sourceaddr, remotesource;
 	bool do_queue_xfrin = false;

 	REQUIRE(DNS_ZONE_VALID(zone));
@@ -12564,7 +12563,8 @@ again:
 	INSIST(dns_remote_count(&zone->primaries) > 0);
 	INSIST(!dns_remote_done(&zone->primaries));

-	sourceaddr = dns_remote_sourceaddr(&zone->primaries);
+	remotesource = dns_remote_sourceaddr(&zone->primaries);
+	sourceaddr = zone_addr_tosockaddr(&zone->sourceaddr);
 	curraddr = dns_remote_curraddr(&zone->primaries);
 	isc_netaddr_fromsockaddr(&primaryip, &curraddr);

@@ -12632,8 +12632,7 @@ again:
 			if (result == ISC_R_SUCCESS && !edns) {
 				DNS_ZONE_SETFLAG(zone, DNS_ZONEFLG_NOEDNS);
 			}
-			result = dns_peer_gettransfersource(peer,
-							    &zone->sourceaddr);
+			result = dns_peer_gettransfersource(peer, &sourceaddr);
 			if (result == ISC_R_SUCCESS) {
 				have_xfrsource = true;
 			}
@@ -12654,9 +12653,10 @@ again:
 			isc_sockaddr_t any;
 			isc_sockaddr_any(&any);

-			zone->sourceaddr = sourceaddr;
+			sourceaddr = remotesource;
 			if (isc_sockaddr_equal(&sourceaddr, &any)) {
-				zone->sourceaddr = zone->xfrsource4;
+				sourceaddr = zone_addr4_tosockaddr(
+					&zone->xfrsource4);
 			}
 		}
 		break;
@@ -12665,15 +12665,17 @@ again:
 			isc_sockaddr_t any;
 			isc_sockaddr_any6(&any);

-			zone->sourceaddr = sourceaddr;
-			if (isc_sockaddr_equal(&zone->sourceaddr, &any)) {
-				zone->sourceaddr = zone->xfrsource6;
+			sourceaddr = remotesource;
+			if (isc_sockaddr_equal(&sourceaddr, &any)) {
+				sourceaddr = zone_addr6_tosockaddr(
+					&zone->xfrsource6);
 			}
 		}
 		break;
 	default:
 		CLEANUP(ISC_R_NOTIMPLEMENTED);
 	}
+	zone->sourceaddr = zone_addr_fromsockaddr(&sourceaddr);

 	/*
 	 * FIXME(OS): This is a bit hackish, but it enforces the SOA query to go
@@ -12703,8 +12705,8 @@ again:
 	const unsigned int connect_timeout = isc_nm_getprimariestimeout() /
 					     MS_PER_SEC;
 	result = dns_request_create(
-		zone->view->requestmgr, message, &zone->sourceaddr, &curraddr,
-		NULL, NULL, options, key, connect_timeout, TCP_REQUEST_TIMEOUT,
+		zone->view->requestmgr, message, &sourceaddr, &curraddr, NULL,
+		NULL, options, key, connect_timeout, TCP_REQUEST_TIMEOUT,
 		UDP_REQUEST_TIMEOUT, UDP_REQUEST_RETRIES, zone->loop,
 		refresh_callback, zone, &zone->request);
 	if (result != ISC_R_SUCCESS) {
@@ -12786,7 +12788,7 @@ ns_query(dns_zone_t *zone, dns_rdataset_t *soardataset, dns_stub_t *stub) {
 	bool have_xfrsource = false;
 	bool reqnsid;
 	uint16_t udpsize = SEND_BUFFER_SIZE;
-	isc_sockaddr_t curraddr, sourceaddr;
+	isc_sockaddr_t curraddr, sourceaddr, remotesource;
 	struct stub_cb_args *cb_args = NULL;

 	REQUIRE(DNS_ZONE_VALID(zone));
@@ -12882,7 +12884,8 @@ ns_query(dns_zone_t *zone, dns_rdataset_t *soardataset, dns_stub_t *stub) {
 	INSIST(dns_remote_count(&zone->primaries) > 0);
 	INSIST(!dns_remote_done(&zone->primaries));

-	sourceaddr = dns_remote_sourceaddr(&zone->primaries);
+	remotesource = dns_remote_sourceaddr(&zone->primaries);
+	sourceaddr = zone_addr_tosockaddr(&zone->sourceaddr);
 	curraddr = dns_remote_curraddr(&zone->primaries);
 	isc_netaddr_fromsockaddr(&primaryip, &curraddr);
 	/*
@@ -12917,8 +12920,7 @@ ns_query(dns_zone_t *zone, dns_rdataset_t *soardataset, dns_stub_t *stub) {
 			if (result == ISC_R_SUCCESS && !edns) {
 				DNS_ZONE_SETFLAG(zone, DNS_ZONEFLG_NOEDNS);
 			}
-			result = dns_peer_gettransfersource(peer,
-							    &zone->sourceaddr);
+			result = dns_peer_gettransfersource(peer, &sourceaddr);
 			if (result == ISC_R_SUCCESS) {
 				have_xfrsource = true;
 			}
@@ -12945,9 +12947,10 @@ ns_query(dns_zone_t *zone, dns_rdataset_t *soardataset, dns_stub_t *stub) {
 			isc_sockaddr_t any;
 			isc_sockaddr_any(&any);

-			zone->sourceaddr = sourceaddr;
-			if (isc_sockaddr_equal(&zone->sourceaddr, &any)) {
-				zone->sourceaddr = zone->xfrsource4;
+			sourceaddr = remotesource;
+			if (isc_sockaddr_equal(&sourceaddr, &any)) {
+				sourceaddr = zone_addr4_tosockaddr(
+					&zone->xfrsource4);
 			}
 		}
 		break;
@@ -12956,9 +12959,10 @@ ns_query(dns_zone_t *zone, dns_rdataset_t *soardataset, dns_stub_t *stub) {
 			isc_sockaddr_t any;
 			isc_sockaddr_any6(&any);

-			zone->sourceaddr = sourceaddr;
-			if (isc_sockaddr_equal(&zone->sourceaddr, &any)) {
-				zone->sourceaddr = zone->xfrsource6;
+			sourceaddr = remotesource;
+			if (isc_sockaddr_equal(&sourceaddr, &any)) {
+				sourceaddr = zone_addr6_tosockaddr(
+					&zone->xfrsource6);
 			}
 		}
 		break;
@@ -12967,6 +12971,7 @@ ns_query(dns_zone_t *zone, dns_rdataset_t *soardataset, dns_stub_t *stub) {
 		POST(result);
 		goto cleanup;
 	}
+	zone->sourceaddr = zone_addr_fromsockaddr(&sourceaddr);

 	/*
 	 * Save request parameters so we can reuse them later on
@@ -12981,8 +12986,8 @@ ns_query(dns_zone_t *zone, dns_rdataset_t *soardataset, dns_stub_t *stub) {
 	cb_args->reqnsid = reqnsid;

 	result = dns_request_create(
-		zone->view->requestmgr, message, &zone->sourceaddr, &curraddr,
-		NULL, NULL, DNS_REQUESTOPT_TCP, key, cb_args->connect_timeout,
+		zone->view->requestmgr, message, &sourceaddr, &curraddr, NULL,
+		NULL, DNS_REQUESTOPT_TCP, key, cb_args->connect_timeout,
 		cb_args->timeout, UDP_REQUEST_TIMEOUT, UDP_REQUEST_RETRIES,
 		zone->loop, stub_callback, cb_args, &zone->request);
 	if (result != ISC_R_SUCCESS) {
@@ -15877,14 +15882,14 @@ next:
 		isc_sockaddr_any(&any);
 		src = zone->primaries.sources[forward->which];
 		if (isc_sockaddr_equal(&src, &any)) {
-			src = zone->xfrsource4;
+			src = zone_addr4_tosockaddr(&zone->xfrsource4);
 		}
 		break;
 	case PF_INET6:
 		isc_sockaddr_any6(&any);
 		src = zone->primaries.sources[forward->which];
 		if (isc_sockaddr_equal(&src, &any)) {
-			src = zone->xfrsource6;
+			src = zone_addr6_tosockaddr(&zone->xfrsource6);
 		}
 		break;
 	default:
@@ -17520,7 +17525,8 @@ checkds_send_toaddr(void *arg) {

 			src = checkds->src;
 			if (isc_sockaddr_equal(&src, &any)) {
-				src = checkds->zone->parentalsrc4;
+				src = zone_addr4_tosockaddr(
+					&checkds->zone->parentalsrc4);
 			}
 		}
 		break;
@@ -17531,7 +17537,8 @@ checkds_send_toaddr(void *arg) {

 			src = checkds->src;
 			if (isc_sockaddr_equal(&src, &any)) {
-				src = checkds->zone->parentalsrc6;
+				src = zone_addr6_tosockaddr(
+					&checkds->zone->parentalsrc6);
 			}
 		}
 		break;
diff --git a/lib/dns/zone_p.h b/lib/dns/zone_p.h
index 52d8d71208..6e8c992ea7 100644
--- a/lib/dns/zone_p.h
+++ b/lib/dns/zone_p.h
@@ -19,12 +19,15 @@
 #include <stdbool.h>

 #include <isc/os.h>
+#include <isc/sockaddr.h>
+#include <isc/util.h>

 #include <dns/adb.h>
 #include <dns/db.h>
 #include <dns/notify.h>
 #include <dns/remote.h>
 #include <dns/update.h>
+#include <dns/zoneaddr.h>
 #include <dns/zonefetch.h>

 /*%
@@ -358,6 +361,74 @@ struct dns_zonemgr {
 	isc_rwlock_t tlsctx_cache_rwlock;
 };

+static inline zone_addr4_t
+zone_addr4_fromsockaddr(const isc_sockaddr_t *sockaddr) {
+	REQUIRE(sockaddr != NULL);
+	REQUIRE(sockaddr->type.sa.sa_family == AF_INET);
+	REQUIRE(sockaddr->type.sin.sin_port == 0);
+	return (zone_addr4_t){
+		.address = sockaddr->type.sin.sin_addr,
+	};
+}
+
+static inline zone_addr6_t
+zone_addr6_fromsockaddr(const isc_sockaddr_t *sockaddr) {
+	REQUIRE(sockaddr != NULL);
+	REQUIRE(sockaddr->type.sa.sa_family == AF_INET6);
+	REQUIRE(sockaddr->type.sin6.sin6_port == 0);
+	REQUIRE(sockaddr->type.sin6.sin6_flowinfo == 0);
+	return (zone_addr6_t){
+		.address = sockaddr->type.sin6.sin6_addr,
+		.scope = sockaddr->type.sin6.sin6_scope_id,
+	};
+}
+
+static inline isc_sockaddr_t
+zone_addr4_tosockaddr(const zone_addr4_t *address) {
+	isc_sockaddr_t sockaddr;
+	isc_sockaddr_fromin(&sockaddr, &address->address, 0);
+	return sockaddr;
+}
+
+static inline isc_sockaddr_t
+zone_addr6_tosockaddr(const zone_addr6_t *address) {
+	isc_sockaddr_t sockaddr;
+	isc_sockaddr_fromin6(&sockaddr, &address->address, 0);
+	sockaddr.type.sin6.sin6_scope_id = address->scope;
+	return sockaddr;
+}
+
+static inline zone_addr_t
+zone_addr_fromsockaddr(const isc_sockaddr_t *sockaddr) {
+	zone_addr_t address = { .family = sockaddr->type.sa.sa_family };
+	switch (address.family) {
+	case AF_INET:
+		address.type.in = zone_addr4_fromsockaddr(sockaddr);
+		break;
+	case AF_INET6:
+		address.type.in6 = zone_addr6_fromsockaddr(sockaddr);
+		break;
+	default:
+		UNREACHABLE();
+	}
+	return address;
+}
+
+static inline isc_sockaddr_t
+zone_addr_tosockaddr(const zone_addr_t *address) {
+	switch (address->family) {
+	case AF_INET:
+		return zone_addr4_tosockaddr(&address->type.in);
+	case AF_INET6:
+		return zone_addr6_tosockaddr(&address->type.in6);
+	case AF_UNSPEC:
+		/* No source has been selected for an operation yet. */
+		return (isc_sockaddr_t){ 0 };
+	default:
+		UNREACHABLE();
+	}
+}
+
 /*%
  * Zone structure.
  */
@@ -443,11 +514,11 @@ struct dns_zone {
 	dns_remote_t cds_endpoints;
 	dns_notifyctx_t notifycds;

-	isc_sockaddr_t parentalsrc4;
-	isc_sockaddr_t parentalsrc6;
-	isc_sockaddr_t xfrsource4;
-	isc_sockaddr_t xfrsource6;
-	isc_sockaddr_t sourceaddr;
+	zone_addr4_t parentalsrc4;
+	zone_addr6_t parentalsrc6;
+	zone_addr4_t xfrsource4;
+	zone_addr6_t xfrsource6;
+	zone_addr_t sourceaddr;
 	dns_tsigkey_t *tsigkey;	    /* key used for xfr */
 	dns_transport_t *transport; /* transport used for xfr */
 	/* Access Control Lists */
diff --git a/lib/dns/zonemgr.c b/lib/dns/zonemgr.c
index a0605630f9..ce21814716 100644
--- a/lib/dns/zonemgr.c
+++ b/lib/dns/zonemgr.c
@@ -387,10 +387,11 @@ got_transfer_quota(void *arg) {

 	primaryaddr = dns_remote_curraddr(&zone->primaries);
 	isc_sockaddr_format(&primaryaddr, primary, sizeof(primary));
+	sourceaddr = zone_addr_tosockaddr(&zone->sourceaddr);
 	if (dns_unreachcache_find(zone->view->unreachcache, &primaryaddr,
-				  &zone->sourceaddr) == ISC_R_SUCCESS)
+				  &sourceaddr) == ISC_R_SUCCESS)
 	{
-		isc_sockaddr_format(&zone->sourceaddr, source, sizeof(source));
+		isc_sockaddr_format(&sourceaddr, source, sizeof(source));
 		dns_zone_logc(zone, DNS_LOGCATEGORY_XFER_IN, ISC_LOG_INFO,
 			      "got_transfer_quota: skipping zone transfer as "
 			      "primary %s (source %s) is unreachable (cached)",
@@ -522,7 +523,7 @@ got_transfer_quota(void *arg) {
 	}

 	LOCK_ZONE(zone);
-	sourceaddr = zone->sourceaddr;
+	sourceaddr = zone_addr_tosockaddr(&zone->sourceaddr);
 	UNLOCK_ZONE(zone);

 	INSIST(isc_sockaddr_pf(&primaryaddr) == isc_sockaddr_pf(&sourceaddr));
diff --git a/lib/dns/zoneproperties.c b/lib/dns/zoneproperties.c
index 5277f4d2a5..a55c7662de 100644
--- a/lib/dns/zoneproperties.c
+++ b/lib/dns/zoneproperties.c
@@ -650,7 +650,7 @@ dns_zone_setxfrsource4(dns_zone_t *zone, const isc_sockaddr_t *xfrsource) {
 	REQUIRE(xfrsource != NULL);

 	LOCK_ZONE(zone);
-	zone->xfrsource4 = *xfrsource;
+	zone->xfrsource4 = zone_addr4_fromsockaddr(xfrsource);
 	UNLOCK_ZONE(zone);
 }

@@ -660,7 +660,7 @@ dns_zone_getxfrsource4(dns_zone_t *zone, isc_sockaddr_t *xfrsource) {
 	REQUIRE(xfrsource != NULL);

 	LOCK_ZONE(zone);
-	*xfrsource = zone->xfrsource4;
+	*xfrsource = zone_addr4_tosockaddr(&zone->xfrsource4);
 	UNLOCK_ZONE(zone);
 }

@@ -670,7 +670,7 @@ dns_zone_setxfrsource6(dns_zone_t *zone, const isc_sockaddr_t *xfrsource) {
 	REQUIRE(xfrsource != NULL);

 	LOCK_ZONE(zone);
-	zone->xfrsource6 = *xfrsource;
+	zone->xfrsource6 = zone_addr6_fromsockaddr(xfrsource);
 	UNLOCK_ZONE(zone);
 }

@@ -680,7 +680,7 @@ dns_zone_getxfrsource6(dns_zone_t *zone, isc_sockaddr_t *xfrsource) {
 	REQUIRE(xfrsource != NULL);

 	LOCK_ZONE(zone);
-	*xfrsource = zone->xfrsource6;
+	*xfrsource = zone_addr6_tosockaddr(&zone->xfrsource6);
 	UNLOCK_ZONE(zone);
 }

@@ -690,7 +690,7 @@ dns_zone_setparentalsrc4(dns_zone_t *zone, const isc_sockaddr_t *parentalsrc) {
 	REQUIRE(parentalsrc != NULL);

 	LOCK_ZONE(zone);
-	zone->parentalsrc4 = *parentalsrc;
+	zone->parentalsrc4 = zone_addr4_fromsockaddr(parentalsrc);
 	UNLOCK_ZONE(zone);
 }

@@ -700,7 +700,7 @@ dns_zone_getparentalsrc4(dns_zone_t *zone, isc_sockaddr_t *parentalsrc) {
 	REQUIRE(parentalsrc != NULL);

 	LOCK_ZONE(zone);
-	*parentalsrc = zone->parentalsrc4;
+	*parentalsrc = zone_addr4_tosockaddr(&zone->parentalsrc4);
 	UNLOCK_ZONE(zone);
 }

@@ -709,7 +709,7 @@ dns_zone_setparentalsrc6(dns_zone_t *zone, const isc_sockaddr_t *parentalsrc) {
 	REQUIRE(DNS_ZONE_VALID(zone));

 	LOCK_ZONE(zone);
-	zone->parentalsrc6 = *parentalsrc;
+	zone->parentalsrc6 = zone_addr6_fromsockaddr(parentalsrc);
 	UNLOCK_ZONE(zone);
 }

@@ -719,7 +719,7 @@ dns_zone_getparentalsrc6(dns_zone_t *zone, isc_sockaddr_t *parentalsrc) {
 	REQUIRE(parentalsrc != NULL);

 	LOCK_ZONE(zone);
-	*parentalsrc = zone->parentalsrc6;
+	*parentalsrc = zone_addr6_tosockaddr(&zone->parentalsrc6);
 	UNLOCK_ZONE(zone);
 }

@@ -732,10 +732,10 @@ dns_zone_setnotifysrc4(dns_zone_t *zone, dns_rdatatype_t type,
 	LOCK_ZONE(zone);
 	switch (type) {
 	case dns_rdatatype_soa:
-		zone->notifysoa.notifysrc4 = *notifysrc;
+		zone->notifysoa.notifysrc4 = zone_addr4_fromsockaddr(notifysrc);
 		break;
 	case dns_rdatatype_cds:
-		zone->notifycds.notifysrc4 = *notifysrc;
+		zone->notifycds.notifysrc4 = zone_addr4_fromsockaddr(notifysrc);
 		break;
 	default:
 		UNREACHABLE();
@@ -752,10 +752,10 @@ dns_zone_setnotifysrc6(dns_zone_t *zone, dns_rdatatype_t type,
 	LOCK_ZONE(zone);
 	switch (type) {
 	case dns_rdatatype_soa:
-		zone->notifysoa.notifysrc6 = *notifysrc;
+		zone->notifysoa.notifysrc6 = zone_addr6_fromsockaddr(notifysrc);
 		break;
 	case dns_rdatatype_cds:
-		zone->notifycds.notifysrc6 = *notifysrc;
+		zone->notifycds.notifysrc6 = zone_addr6_fromsockaddr(notifysrc);
 		break;
 	default:
 		UNREACHABLE();
@@ -1528,7 +1528,7 @@ dns_zone_getsourceaddr(dns_zone_t *zone, isc_sockaddr_t *sourceaddr) {

 	LOCK_ZONE(zone);
 	INSIST(dns_remote_count(&zone->primaries) > 0);
-	*sourceaddr = zone->sourceaddr;
+	*sourceaddr = zone_addr_tosockaddr(&zone->sourceaddr);
 	UNLOCK_ZONE(zone);
 }

diff --git a/tests/dns/zonefile_test.c b/tests/dns/zonefile_test.c
index 44da00d1ff..64ab097fb1 100644
--- a/tests/dns/zonefile_test.c
+++ b/tests/dns/zonefile_test.c
@@ -302,9 +302,148 @@ ISC_LOOP_TEST_IMPL(callbacks) {
 	isc_loopmgr_shutdown();
 }

+static void
+assert_endpoint_equal(const isc_sockaddr_t *actual,
+		      const isc_sockaddr_t *expected) {
+	assert_true(isc_sockaddr_equal(actual, expected));
+	assert_int_equal(actual->length, expected->length);
+	assert_false(ISC_LINK_LINKED(actual, link));
+	if (isc_sockaddr_pf(expected) == PF_INET6) {
+		assert_int_equal(actual->type.sin6.sin6_flowinfo,
+				 expected->type.sin6.sin6_flowinfo);
+	}
+}
+
+ISC_LOOP_TEST_IMPL(addresses) {
+	dns_zone_t *zone = NULL;
+	isc_sockaddr_t addr4, addr6, actual, snapshot;
+	struct in_addr in;
+	struct in6_addr in6;
+	const char *ipv6[] = { "2001:db8::1234", "fe80::1",
+			       "::ffff:192.0.2.1" };
+	const uint32_t scopes[] = { 0, 42, UINT32_MAX };
+	UNUSED(arg);
+
+	assert_int_equal(dns_test_makezone("example", &zone, NULL, false),
+			 ISC_R_SUCCESS);
+
+	/* Newly created zones retain the wildcard defaults of both families. */
+	isc_sockaddr_any(&addr4);
+	isc_sockaddr_any6(&addr6);
+	dns_zone_getxfrsource4(zone, &actual);
+	assert_endpoint_equal(&actual, &addr4);
+	dns_zone_getparentalsrc4(zone, &actual);
+	assert_endpoint_equal(&actual, &addr4);
+	dns_zone_getxfrsource6(zone, &actual);
+	assert_endpoint_equal(&actual, &addr6);
+	dns_zone_getparentalsrc6(zone, &actual);
+	assert_endpoint_equal(&actual, &addr6);
+	dns_zone_setprimaries(zone, &addr4, NULL, NULL, NULL, 1);
+	dns_zone_getsourceaddr(zone, &actual);
+	assert_int_equal(actual.type.sa.sa_family, AF_UNSPEC);
+	assert_int_equal(actual.length, 0);
+
+	assert_int_equal(inet_pton(AF_INET, "192.0.2.123", &in), 1);
+	for (size_t i = 0; i < ARRAY_SIZE(ipv6); i++) {
+		assert_int_equal(inet_pton(AF_INET6, ipv6[i], &in6), 1);
+		isc_sockaddr_fromin(&addr4, &in, 0);
+		isc_sockaddr_fromin6(&addr6, &in6, 0);
+		addr6.type.sin6.sin6_scope_id = scopes[i];
+
+		dns_zone_setxfrsource4(zone, &addr4);
+		dns_zone_setparentalsrc4(zone, &addr4);
+		dns_zone_setxfrsource6(zone, &addr6);
+		dns_zone_setparentalsrc6(zone, &addr6);
+		dns_zone_getxfrsource4(zone, &actual);
+		assert_endpoint_equal(&actual, &addr4);
+		dns_zone_getparentalsrc4(zone, &actual);
+		assert_endpoint_equal(&actual, &addr4);
+		dns_zone_getxfrsource6(zone, &actual);
+		assert_endpoint_equal(&actual, &addr6);
+		dns_zone_getparentalsrc6(zone, &actual);
+		assert_endpoint_equal(&actual, &addr6);
+
+		/* Source snapshots preserve overrides across configuration
+		 * changes. */
+		zone->sourceaddr = zone_addr_fromsockaddr(&addr6);
+		dns_zone_getsourceaddr(zone, &snapshot);
+		assert_endpoint_equal(&snapshot, &addr6);
+		isc_sockaddr_any6(&actual);
+		dns_zone_setxfrsource6(zone, &actual);
+		dns_zone_getsourceaddr(zone, &actual);
+		assert_endpoint_equal(&actual, &snapshot);
+
+		/* Replacing IPv6 with IPv4 must also replace the address
+		 * family. */
+		zone->sourceaddr = zone_addr_fromsockaddr(&addr4);
+		dns_zone_getsourceaddr(zone, &snapshot);
+		assert_endpoint_equal(&snapshot, &addr4);
+		isc_sockaddr_any(&actual);
+		dns_zone_setxfrsource4(zone, &actual);
+		dns_zone_getsourceaddr(zone, &actual);
+		assert_endpoint_equal(&actual, &snapshot);
+	}
+
+	dns_zone_detach(&zone);
+	isc_loopmgr_shutdown();
+}
+
+ISC_LOOP_TEST_IMPL(notify_addresses) {
+	dns_zone_t *zone = NULL;
+	isc_sockaddr_t addr4, addr6, actual;
+	struct in_addr in;
+	struct in6_addr in6;
+	const dns_rdatatype_t types[] = { dns_rdatatype_soa,
+					  dns_rdatatype_cds };
+	UNUSED(arg);
+
+	assert_int_equal(dns_test_makezone("example", &zone, NULL, false),
+			 ISC_R_SUCCESS);
+	isc_sockaddr_any(&addr4);
+	isc_sockaddr_any6(&addr6);
+	for (size_t i = 0; i < ARRAY_SIZE(types); i++) {
+		dns_notifyctx_t *ctx = dns__zone_getnotifyctx(zone, types[i]);
+		actual = zone_addr4_tosockaddr(&ctx->notifysrc4);
+		assert_endpoint_equal(&actual, &addr4);
+		actual = zone_addr6_tosockaddr(&ctx->notifysrc6);
+		assert_endpoint_equal(&actual, &addr6);
+	}
+
+	/* Both contexts keep independent IPv4 and scoped IPv6 sources. */
+	for (size_t i = 0; i < ARRAY_SIZE(types); i++) {
+		assert_int_equal(inet_pton(AF_INET,
+					   i == 0 ? "192.0.2.1" : "192.0.2.2",
+					   &in),
+				 1);
+		assert_int_equal(inet_pton(AF_INET6, "fe80::1", &in6), 1);
+		isc_sockaddr_fromin(&addr4, &in, 0);
+		isc_sockaddr_fromin6(&addr6, &in6, 0);
+		addr6.type.sin6.sin6_scope_id = i == 0 ? 42 : UINT32_MAX;
+		dns_zone_setnotifysrc4(zone, types[i], &addr4);
+		dns_zone_setnotifysrc6(zone, types[i], &addr6);
+	}
+	for (size_t i = 0; i < ARRAY_SIZE(types); i++) {
+		dns_notifyctx_t *ctx = dns__zone_getnotifyctx(zone, types[i]);
+		assert_int_equal(inet_pton(AF_INET,
+					   i == 0 ? "192.0.2.1" : "192.0.2.2",
+					   &in),
+				 1);
+		isc_sockaddr_fromin(&addr4, &in, 0);
+		addr6.type.sin6.sin6_scope_id = i == 0 ? 42 : UINT32_MAX;
+		actual = zone_addr4_tosockaddr(&ctx->notifysrc4);
+		assert_endpoint_equal(&actual, &addr4);
+		actual = zone_addr6_tosockaddr(&ctx->notifysrc6);
+		assert_endpoint_equal(&actual, &addr6);
+	}
+	dns_zone_detach(&zone);
+	isc_loopmgr_shutdown();
+}
+
 ISC_TEST_LIST_START
 ISC_TEST_ENTRY_CUSTOM(filename, setup_test, teardown_test)
 ISC_TEST_ENTRY_CUSTOM(callbacks, setup_test, teardown_test)
+ISC_TEST_ENTRY_CUSTOM(addresses, setup_test, teardown_test)
+ISC_TEST_ENTRY_CUSTOM(notify_addresses, setup_test, teardown_test)
 ISC_TEST_LIST_END

 ISC_TEST_MAIN