@@ -188,7 +188,7 @@ func newOoklaClientRec(uc *ookla.UserConfig) (*ookla.Speedtest, *uploadRecorder)
188188 // unset, so a caller-supplied control (e.g. a future source-interface bind)
189189 // still wins.
190190 if uc != nil && uc .DialerControl == nil {
191- uc .DialerControl = probeDialControl
191+ uc .DialerControl = probeDialControl ()
192192 }
193193 doer := & http.Client {}
194194 client := ookla .New (ookla .WithDoer (doer ), ookla .WithUserConfig (uc ))
@@ -1206,7 +1206,7 @@ func resolveProxiedDest(ctx context.Context, host string) ([]net.IP, error) {
12061206// and under the same relaxation the dial guard honours (probeDialControl set
12071207// nil by allowLoopbackProbes), so loopback-served tests keep working.
12081208func guardProxiedDestination (ctx context.Context , hostport string ) error {
1209- if probeDialControl == nil || len (proxyAddrs ()) == 0 {
1209+ if probeDialControl () == nil || len (proxyAddrs ()) == 0 {
12101210 return nil
12111211 }
12121212 host := hostport
@@ -1270,7 +1270,7 @@ func serverDestination(s *ookla.Server) string {
12701270// makes URL and Host name the same endpoint on the fetchServerList path, so a
12711271// benign Host paired with a hostile URL would otherwise keep its ping.
12721272func guardedServers (ctx context.Context , servers ookla.Servers ) ookla.Servers {
1273- if probeDialControl == nil || len (proxyAddrs ()) == 0 {
1273+ if probeDialControl () == nil || len (proxyAddrs ()) == 0 {
12741274 return servers
12751275 }
12761276 out := make (ookla.Servers , 0 , len (servers ))
@@ -1318,14 +1318,42 @@ func init() {
13181318}
13191319
13201320// probeDialControl is the dial guard the probes AND the measurement client
1321- // install (see probeClient and newOoklaClientRec). A package var for the same
1322- // reason as ooklaPing and fetchServerList: the offline tests serve their fakes
1323- // on loopback, which the guard exists to refuse, so allowLoopbackProbes relaxes
1324- // this one var to cover both the probes and the real transfer. Production never
1325- // reassigns it, and TestProbeRefusesInternalDestinations exercises the real
1326- // probeDialGuard directly so relaxing this in a test cannot hide a regression
1327- // in the guard itself.
1328- var probeDialControl = probeDialGuard
1321+ // install (see probeClient and newOoklaClientRec). A package-level slot for the
1322+ // same reason as ooklaPing and fetchServerList: the offline tests serve their
1323+ // fakes on loopback, which the guard exists to refuse, so allowLoopbackProbes
1324+ // relaxes this one slot to cover both the probes and the real transfer.
1325+ // Production never reassigns it, and TestProbeRefusesInternalDestinations
1326+ // exercises the real probeDialGuard directly so relaxing this in a test cannot
1327+ // hide a regression in the guard itself.
1328+ //
1329+ // An atomic slot, not a plain var: the test swap is not the only traffic. An
1330+ // abandoned run's upload workers keep draining briefly after cancellation and
1331+ // re-enter the transport - and so guardedEnvProxy and this slot - while the
1332+ // next test's helper swaps it back; a plain var there is a data race (caught
1333+ // by the race detector, Aug 2026). Readers capture one coherent value; whether
1334+ // a straggler sees the old guard or the new one, both are safe.
1335+ type probeDialControlFunc = func (network , address string , c syscall.RawConn ) error
1336+
1337+ var probeDialControlSlot atomic.Pointer [probeDialControlFunc ]
1338+
1339+ func init () { setProbeDialControl (probeDialGuard ) }
1340+
1341+ // probeDialControl returns the guard currently installed; nil means the
1342+ // loopback relaxation is in effect.
1343+ func probeDialControl () probeDialControlFunc {
1344+ if p := probeDialControlSlot .Load (); p != nil {
1345+ return * p
1346+ }
1347+ return nil
1348+ }
1349+
1350+ func setProbeDialControl (f probeDialControlFunc ) {
1351+ if f == nil {
1352+ probeDialControlSlot .Store (nil )
1353+ return
1354+ }
1355+ probeDialControlSlot .Store (& f )
1356+ }
13291357
13301358// probeClient is the only client the endpoint probes use. Its dialer refuses
13311359// internal destinations (see probeDialGuard) on every hop, redirects included -
@@ -1344,7 +1372,7 @@ func probeClient(timeout time.Duration) *http.Client {
13441372 // URL and the proxied upload still hit the non-replayable 307 -
13451373 // issues #17/#18, alive behind proxies.
13461374 Proxy : guardedEnvProxy ,
1347- DialContext : (& net.Dialer {Timeout : timeout , Control : probeDialControl }).DialContext ,
1375+ DialContext : (& net.Dialer {Timeout : timeout , Control : probeDialControl () }).DialContext ,
13481376 // Every caller builds this client for a single request and drops it,
13491377 // so a kept-alive socket can never be reused - it would only sit in
13501378 // the abandoned transport's idle pool (zero IdleConnTimeout: forever)
@@ -3415,8 +3443,8 @@ func measurePacketLoss(ctx context.Context, srv *ookla.Server) *float64 {
34153443 // mirrors the library's own default (PacketSendingTimeout).
34163444 analyzer := ookla .NewPacketLossAnalyzer (& ookla.PacketLossAnalyzerOptions {
34173445 SamplingDuration : packetLossSampleDuration ,
3418- TCPDialer : & net.Dialer {Timeout : 5 * time .Second , Control : probeDialControl },
3419- UDPDialer : & net.Dialer {Timeout : 5 * time .Second , Control : probeDialControl },
3446+ TCPDialer : & net.Dialer {Timeout : 5 * time .Second , Control : probeDialControl () },
3447+ UDPDialer : & net.Dialer {Timeout : 5 * time .Second , Control : probeDialControl () },
34203448 })
34213449 var loss * float64
34223450 // Upstream leak (speedtest-go v1.7.11): RunWithContext opens a TCP sampler conn
0 commit comments