From f04339440caf52ce2d8962bb3fcbc4edf091638a Mon Sep 17 00:00:00 2001 From: Jared Mauch Date: Sun, 26 Jul 2026 20:36:30 -0400 Subject: [PATCH] Honor outgoing-interface for TCP/AXFR-over-TLS (#364) Fix SO_REUSEADDR when binding the local source, and key TCP/TLS pipeline reuse by destination and source so transfers do not share a connection across different outgoing interfaces. Addresses one of the items in #364. --- doc/ChangeLog | 7 ++++++ xfrd-tcp.c | 59 +++++++++++++++++++++++++++++++++++++++++++++------ xfrd-tcp.h | 11 ++++++++-- xfrd.c | 27 +++++++++++++++++++---- xfrd.h | 12 +++++++++-- 5 files changed, 101 insertions(+), 15 deletions(-) diff --git a/doc/ChangeLog b/doc/ChangeLog index 17bea4997..6651c4369 100644 --- a/doc/ChangeLog +++ b/doc/ChangeLog @@ -1,3 +1,10 @@ +26 July 2026: Jared + - Fix #364: Make TCP and AXFR-over-TLS transfers honor + outgoing-interface. Fix SO_REUSEADDR when binding the local + source address, and key TCP/TLS pipeline reuse by destination + and local source so connections are not shared across + different outgoing interfaces. + 16 July 2026: Willem - Merge #500: Support for the "oots" SVCB Service Parameter Key diff --git a/xfrd-tcp.c b/xfrd-tcp.c index acea5e2dc..913644490 100644 --- a/xfrd-tcp.c +++ b/xfrd-tcp.c @@ -231,7 +231,26 @@ int password_cb(char *buf, int size, int ATTR_UNUSED(rwflag), void *u) #endif -/* sort tcppipe, first on IP address, for an IPaddresss, sort on num_unused */ +/* Fill addr with the first outgoing-interface matching acl's address family. + * Returns sockaddr length, or 0 if none. Clears addr when returning 0. */ +static socklen_t +xfrd_select_outgoing_sockaddr(struct acl_options* ifc, struct acl_options* acl, +#ifdef INET6 + struct sockaddr_storage* addr) +#else + struct sockaddr_in* addr) +#endif /* INET6 */ +{ + memset(addr, 0, sizeof(*addr)); + while(ifc) { + if(ifc->is_ipv6 == acl->is_ipv6) + return xfrd_acl_sockaddr_frm(ifc, addr); + ifc = ifc->next; + } + return 0; +} + +/* sort tcppipe, first on destination IP, then source IP, then num_unused */ static int xfrd_pipe_cmp(const void* a, const void* b) { @@ -246,6 +265,14 @@ xfrd_pipe_cmp(const void* a, const void* b) r = memcmp(&x->key.ip, &y->key.ip, x->key.ip_len); if(r != 0) return r; + /* also match on local (outgoing-interface) source address */ + if(y->key.src_len != x->key.src_len) + return (int)y->key.src_len - (int)x->key.src_len; + if(x->key.src_len != 0) { + r = memcmp(&x->key.src, &y->key.src, x->key.src_len); + if(r != 0) + return r; + } /* sort that num_unused is sorted ascending, */ if(x->key.num_unused != y->key.num_unused) { return (x->key.num_unused < y->key.num_unused) ? -1 : 1; @@ -636,6 +663,9 @@ pipeline_find(struct xfrd_tcp_set* set, xfrd_zone_type* zone) struct xfrd_tcp_pipeline_key k, *key=&k; key->node.key = key; key->ip_len = xfrd_acl_sockaddr_to(zone->master, &key->ip); + key->src_len = xfrd_select_outgoing_sockaddr( + zone->zone_options->pattern->outgoing_interface, + zone->master, &key->src); key->num_unused = set->tcp_pipeline; /* lookup existing tcp transfer to the master with highest unused */ if(rbtree_find_less_equal(set->pipetree, key, &sme)) { @@ -650,6 +680,12 @@ pipeline_find(struct xfrd_tcp_set* set, xfrd_zone_type* zone) return NULL; if(memcmp(&r->key.ip, &key->ip, key->ip_len) != 0) return NULL; + /* also require the same outgoing-interface (source address) */ + if(r->key.src_len != key->src_len) + return NULL; + if(key->src_len != 0 && + memcmp(&r->key.src, &key->src, key->src_len) != 0) + return NULL; /* correct master, is there a slot free for this transfer? */ if(r->key.num_unused == 0) return NULL; @@ -970,10 +1006,13 @@ xfrd_tcp_open(struct xfrd_tcp_set* set, struct xfrd_tcp_pipeline* tp, } tp->key.ip_len = xfrd_acl_sockaddr_to(zone->master, &tp->key.ip); + tp->key.src_len = 0; + memset(&tp->key.src, 0, sizeof(tp->key.src)); - /* bind it */ + /* bind it (records the successful outgoing-interface in tp->key.src) */ if (!xfrd_bind_local_interface(fd, zone->zone_options->pattern-> - outgoing_interface, zone->master, 1)) { + outgoing_interface, zone->master, 1, + &tp->key.src, &tp->key.src_len)) { close(fd); xfrd_set_refresh_now(zone); return 0; @@ -1702,13 +1741,19 @@ xfrd_tcp_release(struct xfrd_tcp_set* set, xfrd_zone_type* zone) * for the same master, and can fill the unused ID */ if(tp->key.num_unused == 1 && set->tcp_waiting_first) { #ifdef INET6 - struct sockaddr_storage to; + struct sockaddr_storage to, src; #else - struct sockaddr_in to; + struct sockaddr_in to, src; #endif socklen_t to_len = xfrd_acl_sockaddr_to( set->tcp_waiting_first->master, &to); - if(to_len == tp->key.ip_len && memcmp(&to, &tp->key.ip, to_len) == 0) { + socklen_t src_len = xfrd_select_outgoing_sockaddr( + set->tcp_waiting_first->zone_options->pattern-> + outgoing_interface, set->tcp_waiting_first->master, + &src); + if(to_len == tp->key.ip_len && memcmp(&to, &tp->key.ip, to_len) == 0 && + src_len == tp->key.src_len && + (src_len == 0 || memcmp(&src, &tp->key.src, src_len) == 0)) { /* use this connection for the waiting zone */ zone = set->tcp_waiting_first; assert(zone->tcp_conn == -1); @@ -1720,7 +1765,7 @@ xfrd_tcp_release(struct xfrd_tcp_set* set, xfrd_zone_type* zone) pipeline_setup_new_zone(set, tp, zone); return; } - /* waiting zone did not go to same server */ + /* waiting zone did not go to same server/source */ } /* if all unused, or only skipped leftover, close the pipeline */ diff --git a/xfrd-tcp.h b/xfrd-tcp.h index 02970e0d6..e25587e48 100644 --- a/xfrd-tcp.h +++ b/xfrd-tcp.h @@ -95,8 +95,8 @@ struct xfrd_tcp_pipeline_id { }; /** - * The tcp pipeline key structure. By ip_len, ip, num_unused and unique by - * pointer value. + * The tcp pipeline key structure. By ip_len, ip, src_len, src, num_unused + * and unique by pointer value. */ struct xfrd_tcp_pipeline_key { /* the rbtree node, sorted by IP and nr of unused queries */ @@ -108,6 +108,13 @@ struct xfrd_tcp_pipeline_key { struct sockaddr_in ip; #endif /* INET6 */ socklen_t ip_len; + /* local (outgoing-interface) source address; src_len 0 if unbound */ +#ifdef INET6 + struct sockaddr_storage src; +#else + struct sockaddr_in src; +#endif /* INET6 */ + socklen_t src_len; /* number of unused IDs. used IDs are waiting to send their query, * or have been sent but not not all answer packets have been received. * Sorted by num_unused, so a lookup smaller-equal for 65536 finds the diff --git a/xfrd.c b/xfrd.c index 2bd2425f2..71af72d41 100644 --- a/xfrd.c +++ b/xfrd.c @@ -1841,7 +1841,7 @@ xfrd_send_udp(struct acl_options* acl, buffer_type* packet, } /* bind it */ - if (!xfrd_bind_local_interface(fd, ifc, acl, 0)) { + if (!xfrd_bind_local_interface(fd, ifc, acl, 0, NULL, NULL)) { log_msg(LOG_ERR, "xfrd: cannot bind outgoing interface '%s' to " "udp socket: No matching ip addresses found", ifc->ip_address_spec); @@ -1865,7 +1865,13 @@ xfrd_send_udp(struct acl_options* acl, buffer_type* packet, int xfrd_bind_local_interface(int sockd, struct acl_options* ifc, - struct acl_options* acl, int tcp) + struct acl_options* acl, int tcp, +#ifdef INET6 + struct sockaddr_storage* bound_addr, +#else + struct sockaddr_in* bound_addr, +#endif + socklen_t* bound_len) { #ifdef SO_LINGER struct linger linger = {1, 0}; @@ -1878,6 +1884,11 @@ xfrd_bind_local_interface(int sockd, struct acl_options* ifc, #endif /* INET6 */ int ret = 1; + if(bound_len) + *bound_len = 0; + if(bound_addr) + memset(bound_addr, 0, sizeof(*bound_addr)); + if (!ifc) /* no outgoing interface set */ return 1; @@ -1895,8 +1906,11 @@ xfrd_bind_local_interface(int sockd, struct acl_options* ifc, if (tcp) { #ifdef SO_REUSEADDR - if (setsockopt(sockd, SOL_SOCKET, SO_REUSEADDR, &frm, - frm_len) < 0) { + /* Enable address reuse so a fixed source port can be + * rebound after a previous TCP/TLS connection. */ + int on = 1; + if (setsockopt(sockd, SOL_SOCKET, SO_REUSEADDR, &on, + sizeof(on)) < 0) { VERBOSITY(2, (LOG_WARNING, "xfrd: setsockopt " "SO_REUSEADDR failed: %s", strerror(errno))); } @@ -1924,6 +1938,11 @@ xfrd_bind_local_interface(int sockd, struct acl_options* ifc, DEBUG(DEBUG_XFRD,2, (LOG_INFO, "xfrd: bind() %s to %s " "socket was successful", ifc->ip_address_spec, tcp? "tcp":"udp")); + if(bound_addr) { + memcpy(bound_addr, &frm, frm_len); + if(bound_len) + *bound_len = frm_len; + } return 1; } diff --git a/xfrd.h b/xfrd.h index 1e586740d..9c3c42ed4 100644 --- a/xfrd.h +++ b/xfrd.h @@ -463,9 +463,17 @@ xfrd_prepare_updates_for_reload(void); */ void xfrd_prepare_zones_for_reload(void); -/* Bind a local interface to a socket descriptor, return 1 on success */ +/* Bind a local interface to a socket descriptor, return 1 on success. + * If bound_addr is non-NULL, it is filled with the source address that was + * bound (or cleared when no outgoing-interface matched / was configured). */ int xfrd_bind_local_interface(int sockd, struct acl_options* ifc, - struct acl_options* acl, int tcp); + struct acl_options* acl, int tcp, +#ifdef INET6 + struct sockaddr_storage* bound_addr, +#else + struct sockaddr_in* bound_addr, +#endif + socklen_t* bound_len); /* process results and soa info from reload */ void xfrd_process_task_result(xfrd_state_type* xfrd, struct udb_base* taskudb);