DHT: one address family's bucket maintenance starves the other's neighbourhood maintenance
What is the issue?
third-party/dht is pinned at 38c9f261, whose dht_periodic gates neighbourhood maintenance for both address families on a single soon flag that either family's bucket_maintenance can set:
soon |= bucket_maintenance(AF_INET);
soon |= bucket_maintenance(AF_INET6);
if(!soon) {
if(mybucket_grow_time >= now.tv_sec - 150)
soon |= neighbourhood_maintenance(AF_INET);
if(mybucket6_grow_time >= now.tv_sec - 150)
soon |= neighbourhood_maintenance(AF_INET6);
}The two do different jobs: bucket maintenance re-confirms nodes already in the table, neighbourhood maintenance is what grows it. So a family whose nodes never answer stops the other family from finding anyone.
A family gets stuck there permanently, not gradually. buckets6 is calloced so b->time starts at 0, and with max_count == 128 the root bucket is already stale on the first dht_periodic (to = MAX(600 / (128/8), 30) == 37). b->time only advances in new_node() under confirm == 2 — a reply to our own query. If nothing in that family ever replies, bucket_maintenance returns 1 on every round, forever, and the other family never runs neighbourhood maintenance again.
This needs no unusual configuration: a host with no working route for one family still learns that family's addresses, because they arrive in the nodes/ nodes6 of the other family's replies. Three such nodes were enough to reproduce.
Transmission is affected because tr_dht_impl creates a single dual-family instance — libtransmission/tr-dht.cc:162 passes both udp4_socket_ and udp6_socket_ to dht_init(), which is exactly the shape that hits this.
Secondary effect: soon also drives confirm_nodes_time (5–15 s when set, 60–180 s otherwise), so an affected client wakes up far more often while doing less.
Measurements. Offline harness, one host, no public DHT traffic — three unresponsive IPv6 nodes inserted with dht_insert_node, one live local IPv4 responder, 140 s per arm:
| arm | rounds | IPv6 bucket maint. | IPv4 neighbourhood maint. |
|---|---|---|---|
| as shipped, 3 unresponsive IPv6 nodes | 19 | 19/19 | 0 |
| patched, same 3 nodes | 15 | 15/15 | 14/15 |
| control: as shipped, no IPv6 nodes | 16 | 0 | 15/16 |
The IPv6 half is equally dead in the first two arms, which isolates the scheduling as the only variable. Also confirmed on a v4-only OpenWrt router (MT7621) against the live DHT: 23 rounds, IPv6 bucket maintenance claimed 22, neighbourhood maintenance ran once.
Fix, gating each family on its own bucket maintenance and keeping the aggregate for the timer:
soon4 = bucket_maintenance(AF_INET);
soon6 = bucket_maintenance(AF_INET6);
if(!soon4 && mybucket_grow_time >= now.tv_sec - 150)
soon4 |= neighbourhood_maintenance(AF_INET);
if(!soon6 && mybucket6_grow_time >= now.tv_sec - 150)
soon6 |= neighbourhood_maintenance(AF_INET6);
soon = soon4 | soon6;Within each family either bucket or neighbourhood maintenance runs, never both, so a round still sends at most two maintenance queries — the query budget is unchanged, the work is just redistributed.
Fixed upstream in jech/dht as be5684a. transmission/dht#6 cherry-picks it onto post-0.27-transmission, together with c723931, an unrelated one-character fix merged upstream alongside it where search_step could return without sending on a wakeup scheduled for exactly the right second. @tearfur — happy to raise the submodule bump here as a follow-up once that lands, or to leave it to you.
Possibly related: #6878 reports an IPv6-only host where DHT is suspected; this would be the mirror case.
Which application of Transmission?
transmission-daemon
Which version of Transmission?
All, including main — third-party/dht @ jech/dht@38c9f261
Source: transmission/transmission