Captive portal detection was half-migrated: it had a build tag and buildfeatures constant, but its code still lived in build-tag-gated files in ipn/ipnlocal and net/netcheck, with its per-backend state (context, cancel func, signaling channel) as fields on LocalBackend. Move it under feature/captiveportal. The health-driven detection loop becomes an ipnext.Extension holding its own state: it starts and stops the loop from the BackendStateChange hook and subscribes to health.Change events on the eventbus itself, removing the captive portal hooks and special cases from LocalBackend entirely. The DERP map now comes from a new ipnext.NodeBackend.DERPMap method, and the preferred DERP region from magicsock's last netcheck report (the same underlying source as the previously used Hostinfo.NetInfo). The netcheck probe hook is now exported with a signature free of netcheck internals, and its implementation moves to the small feature/captiveportal/netcheckhook package, which installs the hook as an import side effect. That package stays free of tsd/wgengine dependencies so the tailscale CLI can keep probing for captive portals in "tailscale netcheck" without linking the daemon-side extension. The net/captivedetection library itself is unchanged and stays put; after this change it is only linked when something pulls in netcheckhook or the feature extension. tailscaled links the feature by default via condregister as before, but tsnet no longer does (shrinking tsnet, k8s-operator, and tsidp); tsnet users who want it can blank-import the feature package, and tsnet's dep test now locks that in. Updates #12614 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com> Change-Id: I3f1d09f9dc03e18f9a648ab5e42d16fa540b3fa9
68 lines
1.9 KiB
Go
68 lines
1.9 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
// Package netcheckhook makes netcheck probe for captive portals during
|
|
// full reports. It does so as a side effect of being imported, by
|
|
// installing a netcheck hook from init.
|
|
package netcheckhook
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"time"
|
|
|
|
"tailscale.com/net/captivedetection"
|
|
"tailscale.com/net/netcheck"
|
|
"tailscale.com/tailcfg"
|
|
)
|
|
|
|
func init() {
|
|
netcheck.HookStartCaptivePortalDetection.Set(startCaptivePortalDetection)
|
|
}
|
|
|
|
// captivePortalDelay is the duration to wait after starting a netcheck before
|
|
// also probing for a captive portal, to let UDP STUN finish first and avoid
|
|
// the probe if it's unnecessary. Chosen semi-arbitrarily.
|
|
const captivePortalDelay = 200 * time.Millisecond
|
|
|
|
func startCaptivePortalDetection(ctx context.Context, c *netcheck.Client, dm *tailcfg.DERPMap, preferredDERP int, setCaptivePortal func(bool)) (done <-chan struct{}, stop func()) {
|
|
logf := c.Logf
|
|
if logf == nil {
|
|
logf = log.Printf
|
|
}
|
|
|
|
// This goroutine can't be tracked by the wait group that
|
|
// netcheck.GetReport uses for its probes, since GetReport doesn't
|
|
// wait for that group to finish before returning and we'd get a
|
|
// data race. Instead, completion is signaled by closing ch, which
|
|
// GetReport receives as the done channel.
|
|
ch := make(chan struct{})
|
|
|
|
tmr := time.AfterFunc(captivePortalDelay, func() {
|
|
defer close(ch)
|
|
d := captivedetection.NewDetector(logf)
|
|
found := d.Detect(ctx, c.NetMon, dm, preferredDERP)
|
|
setCaptivePortal(found)
|
|
})
|
|
|
|
if c.Verbose {
|
|
// Don't cancel our captive portal check if we're
|
|
// explicitly doing a verbose netcheck.
|
|
return ch, func() {}
|
|
}
|
|
|
|
stop = func() {
|
|
if tmr.Stop() {
|
|
// Stopped successfully; need to close the
|
|
// signal channel ourselves.
|
|
close(ch)
|
|
return
|
|
}
|
|
|
|
// Did not stop; do nothing and it'll finish by itself
|
|
// and close the signal channel.
|
|
}
|
|
|
|
return ch, stop
|
|
}
|