syncs, all: move to using Go's new atomic types instead of ours

Fixes #5185

Change-Id: I850dd532559af78c3895e2924f8237ccc328449d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2022-08-03 21:51:02 -07:00 committed by Brad Fitzpatrick
parent 9bb5a038e5
commit 4950fe60bd
20 changed files with 101 additions and 143 deletions

View File

@ -29,7 +29,6 @@
"tailscale.com/ipn" "tailscale.com/ipn"
"tailscale.com/paths" "tailscale.com/paths"
"tailscale.com/safesocket" "tailscale.com/safesocket"
"tailscale.com/syncs"
"tailscale.com/version/distro" "tailscale.com/version/distro"
) )
@ -230,8 +229,6 @@ func fatalf(format string, a ...any) {
socket string socket string
} }
var gotSignal syncs.AtomicBool
func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context, context.CancelFunc) { func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context, context.CancelFunc) {
s := safesocket.DefaultConnectionStrategy(rootArgs.socket) s := safesocket.DefaultConnectionStrategy(rootArgs.socket)
c, err := safesocket.Connect(s) c, err := safesocket.Connect(s)
@ -257,7 +254,6 @@ func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context
signal.Reset(syscall.SIGINT, syscall.SIGTERM) signal.Reset(syscall.SIGINT, syscall.SIGTERM)
return return
} }
gotSignal.Set(true)
c.Close() c.Close()
cancel() cancel()
}() }()

View File

@ -71,7 +71,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+ 💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
tailscale.com/paths from tailscale.com/cmd/tailscale/cli+ tailscale.com/paths from tailscale.com/cmd/tailscale/cli+
tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli+ tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli+
tailscale.com/syncs from tailscale.com/net/interfaces+ tailscale.com/syncs from tailscale.com/net/netcheck
tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+ tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+
tailscale.com/tka from tailscale.com/types/key tailscale.com/tka from tailscale.com/types/key
W tailscale.com/tsconst from tailscale.com/net/interfaces W tailscale.com/tsconst from tailscale.com/net/interfaces

View File

@ -241,7 +241,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/safesocket from tailscale.com/client/tailscale+ tailscale.com/safesocket from tailscale.com/client/tailscale+
tailscale.com/smallzstd from tailscale.com/ipn/ipnserver+ tailscale.com/smallzstd from tailscale.com/ipn/ipnserver+
LD 💣 tailscale.com/ssh/tailssh from tailscale.com/cmd/tailscaled LD 💣 tailscale.com/ssh/tailssh from tailscale.com/cmd/tailscaled
tailscale.com/syncs from tailscale.com/control/controlknobs+ tailscale.com/syncs from tailscale.com/net/netcheck+
tailscale.com/tailcfg from tailscale.com/client/tailscale/apitype+ tailscale.com/tailcfg from tailscale.com/client/tailscale/apitype+
LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh
tailscale.com/tka from tailscale.com/types/key+ tailscale.com/tka from tailscale.com/types/key+

View File

@ -7,12 +7,13 @@
package controlknobs package controlknobs
import ( import (
"sync/atomic"
"tailscale.com/envknob" "tailscale.com/envknob"
"tailscale.com/syncs"
) )
// disableUPnP indicates whether to attempt UPnP mapping. // disableUPnP indicates whether to attempt UPnP mapping.
var disableUPnP syncs.AtomicBool var disableUPnP atomic.Bool
func init() { func init() {
SetDisableUPnP(envknob.Bool("TS_DISABLE_UPNP")) SetDisableUPnP(envknob.Bool("TS_DISABLE_UPNP"))
@ -21,11 +22,11 @@ func init() {
// DisableUPnP reports the last reported value from control // DisableUPnP reports the last reported value from control
// whether UPnP portmapping should be disabled. // whether UPnP portmapping should be disabled.
func DisableUPnP() bool { func DisableUPnP() bool {
return disableUPnP.Get() return disableUPnP.Load()
} }
// SetDisableUPnP sets whether control says that UPnP should be // SetDisableUPnP sets whether control says that UPnP should be
// disabled. // disabled.
func SetDisableUPnP(v bool) { func SetDisableUPnP(v bool) {
disableUPnP.Set(v) disableUPnP.Store(v)
} }

View File

@ -41,7 +41,6 @@
"tailscale.com/disco" "tailscale.com/disco"
"tailscale.com/envknob" "tailscale.com/envknob"
"tailscale.com/metrics" "tailscale.com/metrics"
"tailscale.com/syncs"
"tailscale.com/types/key" "tailscale.com/types/key"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/types/pad32" "tailscale.com/types/pad32"
@ -232,7 +231,7 @@ type dupClientSet struct {
} }
func (s *dupClientSet) ActiveClient() *sclient { func (s *dupClientSet) ActiveClient() *sclient {
if s.last != nil && !s.last.isDisabled.Get() { if s.last != nil && !s.last.isDisabled.Load() {
return s.last return s.last
} }
return nil return nil
@ -499,8 +498,8 @@ func (s *Server) registerClient(c *sclient) {
s.dupClientConns.Add(2) // both old and new count s.dupClientConns.Add(2) // both old and new count
s.dupClientConnTotal.Add(1) s.dupClientConnTotal.Add(1)
old := set.ActiveClient() old := set.ActiveClient()
old.isDup.Set(true) old.isDup.Store(true)
c.isDup.Set(true) c.isDup.Store(true)
s.clients[c.key] = &dupClientSet{ s.clients[c.key] = &dupClientSet{
last: c, last: c,
set: map[*sclient]bool{ set: map[*sclient]bool{
@ -512,7 +511,7 @@ func (s *Server) registerClient(c *sclient) {
case *dupClientSet: case *dupClientSet:
s.dupClientConns.Add(1) // the gauge s.dupClientConns.Add(1) // the gauge
s.dupClientConnTotal.Add(1) // the counter s.dupClientConnTotal.Add(1) // the counter
c.isDup.Set(true) c.isDup.Store(true)
set.set[c] = true set.set[c] = true
set.last = c set.last = c
set.sendHistory = append(set.sendHistory, c) set.sendHistory = append(set.sendHistory, c)
@ -571,8 +570,8 @@ func (s *Server) unregisterClient(c *sclient) {
if remain == nil { if remain == nil {
panic("unexpected nil remain from single element dup set") panic("unexpected nil remain from single element dup set")
} }
remain.isDisabled.Set(false) remain.isDisabled.Store(false)
remain.isDup.Set(false) remain.isDup.Store(false)
s.clients[c.key] = singleClient{remain} s.clients[c.key] = singleClient{remain}
} }
} }
@ -1073,11 +1072,11 @@ func (s *Server) sendServerKey(lw *lazyBufioWriter) error {
} }
func (s *Server) noteClientActivity(c *sclient) { func (s *Server) noteClientActivity(c *sclient) {
if !c.isDup.Get() { if !c.isDup.Load() {
// Fast path for clients that aren't in a dup set. // Fast path for clients that aren't in a dup set.
return return
} }
if c.isDisabled.Get() { if c.isDisabled.Load() {
// If they're already disabled, no point checking more. // If they're already disabled, no point checking more.
return return
} }
@ -1112,7 +1111,7 @@ func (s *Server) noteClientActivity(c *sclient) {
for _, prior := range ds.sendHistory { for _, prior := range ds.sendHistory {
if prior == c { if prior == c {
ds.ForeachClient(func(c *sclient) { ds.ForeachClient(func(c *sclient) {
c.isDisabled.Set(true) c.isDisabled.Store(true)
}) })
break break
} }
@ -1253,8 +1252,8 @@ type sclient struct {
peerGone chan key.NodePublic // write request that a previous sender has disconnected (not used by mesh peers) peerGone chan key.NodePublic // write request that a previous sender has disconnected (not used by mesh peers)
meshUpdate chan struct{} // write request to write peerStateChange meshUpdate chan struct{} // write request to write peerStateChange
canMesh bool // clientInfo had correct mesh token for inter-region routing canMesh bool // clientInfo had correct mesh token for inter-region routing
isDup syncs.AtomicBool // whether more than 1 sclient for key is connected isDup atomic.Bool // whether more than 1 sclient for key is connected
isDisabled syncs.AtomicBool // whether sends to this peer are disabled due to active/active dups isDisabled atomic.Bool // whether sends to this peer are disabled due to active/active dups
// replaceLimiter controls how quickly two connections with // replaceLimiter controls how quickly two connections with
// the same client key can kick each other off the server by // the same client key can kick each other off the server by

View File

@ -958,10 +958,10 @@ func TestServerDupClients(t *testing.T) {
t.Error("wrong single client") t.Error("wrong single client")
return return
} }
if want.isDup.Get() { if want.isDup.Load() {
t.Errorf("unexpected isDup on singleClient") t.Errorf("unexpected isDup on singleClient")
} }
if want.isDisabled.Get() { if want.isDisabled.Load() {
t.Errorf("unexpected isDisabled on singleClient") t.Errorf("unexpected isDisabled on singleClient")
} }
case nil: case nil:
@ -1004,13 +1004,13 @@ func TestServerDupClients(t *testing.T) {
} }
checkDup := func(t *testing.T, c *sclient, want bool) { checkDup := func(t *testing.T, c *sclient, want bool) {
t.Helper() t.Helper()
if got := c.isDup.Get(); got != want { if got := c.isDup.Load(); got != want {
t.Errorf("client %q isDup = %v; want %v", clientName[c], got, want) t.Errorf("client %q isDup = %v; want %v", clientName[c], got, want)
} }
} }
checkDisabled := func(t *testing.T, c *sclient, want bool) { checkDisabled := func(t *testing.T, c *sclient, want bool) {
t.Helper() t.Helper()
if got := c.isDisabled.Get(); got != want { if got := c.isDisabled.Load(); got != want {
t.Errorf("client %q isDisabled = %v; want %v", clientName[c], got, want) t.Errorf("client %q isDisabled = %v; want %v", clientName[c], got, want)
} }
} }

View File

@ -40,7 +40,6 @@
"tailscale.com/net/tsdial" "tailscale.com/net/tsdial"
"tailscale.com/paths" "tailscale.com/paths"
"tailscale.com/portlist" "tailscale.com/portlist"
"tailscale.com/syncs"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/tka" "tailscale.com/tka"
"tailscale.com/types/dnstype" "tailscale.com/types/dnstype"
@ -127,7 +126,7 @@ type LocalBackend struct {
serverURL string // tailcontrol URL serverURL string // tailcontrol URL
newDecompressor func() (controlclient.Decompressor, error) newDecompressor func() (controlclient.Decompressor, error)
varRoot string // or empty if SetVarRoot never called varRoot string // or empty if SetVarRoot never called
sshAtomicBool syncs.AtomicBool sshAtomicBool atomic.Bool
shutdownCalled bool // if Shutdown has been called shutdownCalled bool // if Shutdown has been called
filterAtomic atomic.Pointer[filter.Filter] filterAtomic atomic.Pointer[filter.Filter]
@ -1740,7 +1739,7 @@ func (b *LocalBackend) loadStateLocked(key ipn.StateKey, prefs *ipn.Prefs) (err
// setAtomicValuesFromPrefs populates sshAtomicBool and containsViaIPFuncAtomic // setAtomicValuesFromPrefs populates sshAtomicBool and containsViaIPFuncAtomic
// from the prefs p, which may be nil. // from the prefs p, which may be nil.
func (b *LocalBackend) setAtomicValuesFromPrefs(p *ipn.Prefs) { func (b *LocalBackend) setAtomicValuesFromPrefs(p *ipn.Prefs) {
b.sshAtomicBool.Set(p != nil && p.RunSSH && canSSH) b.sshAtomicBool.Store(p != nil && p.RunSSH && canSSH)
if p == nil { if p == nil {
b.containsViaIPFuncAtomic.Store(tsaddr.NewContainsIPFunc(nil)) b.containsViaIPFuncAtomic.Store(tsaddr.NewContainsIPFunc(nil))
@ -3053,7 +3052,7 @@ func (b *LocalBackend) ResetForClientDisconnect() {
b.setAtomicValuesFromPrefs(nil) b.setAtomicValuesFromPrefs(nil)
} }
func (b *LocalBackend) ShouldRunSSH() bool { return b.sshAtomicBool.Get() && canSSH } func (b *LocalBackend) ShouldRunSSH() bool { return b.sshAtomicBool.Load() && canSSH }
// ShouldHandleViaIP reports whether whether ip is an IPv6 address in the // ShouldHandleViaIP reports whether whether ip is an IPv6 address in the
// Tailscale ULA's v6 "via" range embedding an IPv4 address to be forwarded to // Tailscale ULA's v6 "via" range embedding an IPv4 address to be forwarded to

View File

@ -26,6 +26,7 @@
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"unicode" "unicode"
"unicode/utf8" "unicode/utf8"
@ -41,7 +42,6 @@
"tailscale.com/net/interfaces" "tailscale.com/net/interfaces"
"tailscale.com/net/netaddr" "tailscale.com/net/netaddr"
"tailscale.com/net/netutil" "tailscale.com/net/netutil"
"tailscale.com/syncs"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/util/clientmetric" "tailscale.com/util/clientmetric"
"tailscale.com/wgengine" "tailscale.com/wgengine"
@ -58,7 +58,7 @@ type peerAPIServer struct {
b *LocalBackend b *LocalBackend
rootDir string // empty means file receiving unavailable rootDir string // empty means file receiving unavailable
selfNode *tailcfg.Node selfNode *tailcfg.Node
knownEmpty syncs.AtomicBool knownEmpty atomic.Bool
resolver *resolver.Resolver resolver *resolver.Resolver
// directFileMode is whether we're writing files directly to a // directFileMode is whether we're writing files directly to a
@ -144,7 +144,7 @@ func (s *peerAPIServer) hasFilesWaiting() bool {
if s == nil || s.rootDir == "" || s.directFileMode { if s == nil || s.rootDir == "" || s.directFileMode {
return false return false
} }
if s.knownEmpty.Get() { if s.knownEmpty.Load() {
// Optimization: this is usually empty, so avoid opening // Optimization: this is usually empty, so avoid opening
// the directory and checking. We can't cache the actual // the directory and checking. We can't cache the actual
// has-files-or-not values as the macOS/iOS client might // has-files-or-not values as the macOS/iOS client might
@ -185,7 +185,7 @@ func (s *peerAPIServer) hasFilesWaiting() bool {
} }
} }
if err == io.EOF { if err == io.EOF {
s.knownEmpty.Set(true) s.knownEmpty.Store(true)
} }
if err != nil { if err != nil {
break break
@ -808,7 +808,7 @@ func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
// TODO: some real response // TODO: some real response
success = true success = true
io.WriteString(w, "{}\n") io.WriteString(w, "{}\n")
h.ps.knownEmpty.Set(false) h.ps.knownEmpty.Store(false)
h.ps.b.sendFileNotify() h.ps.b.sendFileNotify()
} }

View File

@ -504,7 +504,7 @@ func TestDeletedMarkers(t *testing.T) {
nothingWaiting := func() { nothingWaiting := func() {
t.Helper() t.Helper()
ps.knownEmpty.Set(false) ps.knownEmpty.Store(false)
if ps.hasFilesWaiting() { if ps.hasFilesWaiting() {
t.Fatal("unexpected files waiting") t.Fatal("unexpected files waiting")
} }

View File

@ -7,6 +7,7 @@
import ( import (
"context" "context"
"sync" "sync"
"sync/atomic"
"testing" "testing"
"time" "time"
@ -15,7 +16,6 @@
"tailscale.com/control/controlclient" "tailscale.com/control/controlclient"
"tailscale.com/ipn" "tailscale.com/ipn"
"tailscale.com/ipn/store/mem" "tailscale.com/ipn/store/mem"
"tailscale.com/syncs"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/empty" "tailscale.com/types/empty"
"tailscale.com/types/key" "tailscale.com/types/key"
@ -91,7 +91,7 @@ type mockControl struct {
opts controlclient.Options opts controlclient.Options
logfActual logger.Logf logfActual logger.Logf
statusFunc func(controlclient.Status) statusFunc func(controlclient.Status)
preventLog syncs.AtomicBool preventLog atomic.Bool
mu sync.Mutex mu sync.Mutex
calls []string calls []string
@ -108,7 +108,7 @@ func newMockControl(tb testing.TB) *mockControl {
} }
func (cc *mockControl) logf(format string, args ...any) { func (cc *mockControl) logf(format string, args ...any) {
if cc.preventLog.Get() || cc.logfActual == nil { if cc.preventLog.Load() || cc.logfActual == nil {
return return
} }
cc.logfActual(format, args...) cc.logfActual(format, args...)
@ -292,7 +292,7 @@ func TestStateMachine(t *testing.T) {
cc := newMockControl(t) cc := newMockControl(t)
cc.statusFunc = b.setClientStatus cc.statusFunc = b.setClientStatus
t.Cleanup(func() { cc.preventLog.Set(true) }) // hacky way to pacify issue 3020 t.Cleanup(func() { cc.preventLog.Store(true) }) // hacky way to pacify issue 3020
b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) { b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) {
cc.mu.Lock() cc.mu.Lock()
@ -311,7 +311,7 @@ func TestStateMachine(t *testing.T) {
notifies.expect(0) notifies.expect(0)
b.SetNotifyCallback(func(n ipn.Notify) { b.SetNotifyCallback(func(n ipn.Notify) {
if cc.preventLog.Get() { if cc.preventLog.Load() {
return return
} }
if n.State != nil || if n.State != nil ||
@ -920,7 +920,7 @@ func TestStateMachine(t *testing.T) {
type testStateStorage struct { type testStateStorage struct {
mem mem.Store mem mem.Store
written syncs.AtomicBool written atomic.Bool
} }
func (s *testStateStorage) ReadState(id ipn.StateKey) ([]byte, error) { func (s *testStateStorage) ReadState(id ipn.StateKey) ([]byte, error) {
@ -928,18 +928,18 @@ func (s *testStateStorage) ReadState(id ipn.StateKey) ([]byte, error) {
} }
func (s *testStateStorage) WriteState(id ipn.StateKey, bs []byte) error { func (s *testStateStorage) WriteState(id ipn.StateKey, bs []byte) error {
s.written.Set(true) s.written.Store(true)
return s.mem.WriteState(id, bs) return s.mem.WriteState(id, bs)
} }
// awaitWrite clears the "I've seen writes" bit, in prep for a future // awaitWrite clears the "I've seen writes" bit, in prep for a future
// call to sawWrite to see if a write arrived. // call to sawWrite to see if a write arrived.
func (s *testStateStorage) awaitWrite() { s.written.Set(false) } func (s *testStateStorage) awaitWrite() { s.written.Store(false) }
// sawWrite reports whether there's been a WriteState call since the most // sawWrite reports whether there's been a WriteState call since the most
// recent awaitWrite call. // recent awaitWrite call.
func (s *testStateStorage) sawWrite() bool { func (s *testStateStorage) sawWrite() bool {
v := s.written.Get() v := s.written.Load()
s.awaitWrite() s.awaitWrite()
return v return v
} }

View File

@ -23,7 +23,6 @@
"tailscale.com/logtail/backoff" "tailscale.com/logtail/backoff"
"tailscale.com/net/interfaces" "tailscale.com/net/interfaces"
"tailscale.com/syncs"
tslogger "tailscale.com/types/logger" tslogger "tailscale.com/types/logger"
"tailscale.com/wgengine/monitor" "tailscale.com/wgengine/monitor"
) )
@ -448,15 +447,15 @@ func (l *Logger) Flush() error {
} }
// logtailDisabled is whether logtail uploads to logcatcher are disabled. // logtailDisabled is whether logtail uploads to logcatcher are disabled.
var logtailDisabled syncs.AtomicBool var logtailDisabled atomic.Bool
// Disable disables logtail uploads for the lifetime of the process. // Disable disables logtail uploads for the lifetime of the process.
func Disable() { func Disable() {
logtailDisabled.Set(true) logtailDisabled.Store(true)
} }
func (l *Logger) sendLocked(jsonBlob []byte) (int, error) { func (l *Logger) sendLocked(jsonBlob []byte) (int, error) {
if logtailDisabled.Get() { if logtailDisabled.Load() {
return len(jsonBlob), nil return len(jsonBlob), nil
} }
n, err := l.buffer.Write(jsonBlob) n, err := l.buffer.Write(jsonBlob)

View File

@ -17,13 +17,13 @@
"os/exec" "os/exec"
"runtime" "runtime"
"strings" "strings"
"sync/atomic"
"github.com/jsimonetti/rtnetlink" "github.com/jsimonetti/rtnetlink"
"github.com/mdlayher/netlink" "github.com/mdlayher/netlink"
"go4.org/mem" "go4.org/mem"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"tailscale.com/net/netaddr" "tailscale.com/net/netaddr"
"tailscale.com/syncs"
"tailscale.com/util/lineread" "tailscale.com/util/lineread"
) )
@ -31,7 +31,7 @@ func init() {
likelyHomeRouterIP = likelyHomeRouterIPLinux likelyHomeRouterIP = likelyHomeRouterIPLinux
} }
var procNetRouteErr syncs.AtomicBool var procNetRouteErr atomic.Bool
// errStopReading is a sentinel error value used internally by // errStopReading is a sentinel error value used internally by
// lineread.File callers to stop reading. It doesn't escape to // lineread.File callers to stop reading. It doesn't escape to
@ -47,7 +47,7 @@ func init() {
ens18 0000000A 00000000 0001 0 0 0 0000FFFF 0 0 0 ens18 0000000A 00000000 0001 0 0 0 0000FFFF 0 0 0
*/ */
func likelyHomeRouterIPLinux() (ret netip.Addr, ok bool) { func likelyHomeRouterIPLinux() (ret netip.Addr, ok bool) {
if procNetRouteErr.Get() { if procNetRouteErr.Load() {
// If we failed to read /proc/net/route previously, don't keep trying. // If we failed to read /proc/net/route previously, don't keep trying.
// But if we're on Android, go into the Android path. // But if we're on Android, go into the Android path.
if runtime.GOOS == "android" { if runtime.GOOS == "android" {
@ -93,7 +93,7 @@ func likelyHomeRouterIPLinux() (ret netip.Addr, ok bool) {
err = nil err = nil
} }
if err != nil { if err != nil {
procNetRouteErr.Set(true) procNetRouteErr.Store(true)
if runtime.GOOS == "android" { if runtime.GOOS == "android" {
return likelyHomeRouterIPAndroid() return likelyHomeRouterIPAndroid()
} }

View File

@ -18,25 +18,25 @@
"context" "context"
"net" "net"
"net/netip" "net/netip"
"sync/atomic"
"tailscale.com/net/netknob" "tailscale.com/net/netknob"
"tailscale.com/syncs"
"tailscale.com/types/logger" "tailscale.com/types/logger"
) )
var disabled syncs.AtomicBool var disabled atomic.Bool
// SetEnabled enables or disables netns for the process. // SetEnabled enables or disables netns for the process.
// It defaults to being enabled. // It defaults to being enabled.
func SetEnabled(on bool) { func SetEnabled(on bool) {
disabled.Set(!on) disabled.Store(!on)
} }
// Listener returns a new net.Listener with its Control hook func // Listener returns a new net.Listener with its Control hook func
// initialized as necessary to run in logical network namespace that // initialized as necessary to run in logical network namespace that
// doesn't route back into Tailscale. // doesn't route back into Tailscale.
func Listener(logf logger.Logf) *net.ListenConfig { func Listener(logf logger.Logf) *net.ListenConfig {
if disabled.Get() { if disabled.Load() {
return new(net.ListenConfig) return new(net.ListenConfig)
} }
return &net.ListenConfig{Control: control(logf)} return &net.ListenConfig{Control: control(logf)}
@ -57,7 +57,7 @@ func NewDialer(logf logger.Logf) Dialer {
// handles using a SOCKS if configured in the environment with // handles using a SOCKS if configured in the environment with
// ALL_PROXY. // ALL_PROXY.
func FromDialer(logf logger.Logf, d *net.Dialer) Dialer { func FromDialer(logf logger.Logf, d *net.Dialer) Dialer {
if disabled.Get() { if disabled.Load() {
return d return d
} }
d.Control = control(logf) d.Control = control(logf)

View File

@ -12,10 +12,10 @@
"net/http/httptest" "net/http/httptest"
"net/netip" "net/netip"
"sync" "sync"
"sync/atomic"
"testing" "testing"
"tailscale.com/net/netaddr" "tailscale.com/net/netaddr"
"tailscale.com/syncs"
"tailscale.com/types/logger" "tailscale.com/types/logger"
) )
@ -26,7 +26,7 @@ type TestIGD struct {
pxpConn net.PacketConn // for NAT-PMP and/or PCP pxpConn net.PacketConn // for NAT-PMP and/or PCP
ts *httptest.Server ts *httptest.Server
logf logger.Logf logf logger.Logf
closed syncs.AtomicBool closed atomic.Bool
// do* will log which packets are sent, but will not reply to unexpected packets. // do* will log which packets are sent, but will not reply to unexpected packets.
@ -71,7 +71,7 @@ func NewTestIGD(logf logger.Logf, t TestIGDOptions) (*TestIGD, error) {
d.logf = func(msg string, args ...any) { d.logf = func(msg string, args ...any) {
// Don't log after the device has closed; // Don't log after the device has closed;
// stray trailing logging angers testing.T.Logf. // stray trailing logging angers testing.T.Logf.
if d.closed.Get() { if d.closed.Load() {
return return
} }
logf(msg, args...) logf(msg, args...)
@ -107,7 +107,7 @@ func testIPAndGateway() (gw, ip netip.Addr, ok bool) {
} }
func (d *TestIGD) Close() error { func (d *TestIGD) Close() error {
d.closed.Set(true) d.closed.Store(true)
d.ts.Close() d.ts.Close()
d.upnpConn.Close() d.upnpConn.Close()
d.pxpConn.Close() d.pxpConn.Close()
@ -135,7 +135,7 @@ func (d *TestIGD) serveUPnPDiscovery() {
for { for {
n, src, err := d.upnpConn.ReadFrom(buf) n, src, err := d.upnpConn.ReadFrom(buf)
if err != nil { if err != nil {
if !d.closed.Get() { if !d.closed.Load() {
d.logf("serveUPnP failed: %v", err) d.logf("serveUPnP failed: %v", err)
} }
return return
@ -162,7 +162,7 @@ func (d *TestIGD) servePxP() {
for { for {
n, a, err := d.pxpConn.ReadFrom(buf) n, a, err := d.pxpConn.ReadFrom(buf)
if err != nil { if err != nil {
if !d.closed.Get() { if !d.closed.Load() {
d.logf("servePxP failed: %v", err) d.logf("servePxP failed: %v", err)
} }
return return

View File

@ -14,12 +14,12 @@
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"sync/atomic"
"syscall" "syscall"
"time" "time"
"go4.org/mem" "go4.org/mem"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"tailscale.com/syncs"
) )
// Reading the sockfiles on Linux is very fast, so we can do it often. // Reading the sockfiles on Linux is very fast, so we can do it often.
@ -27,7 +27,7 @@
var sockfiles = []string{"/proc/net/tcp", "/proc/net/tcp6", "/proc/net/udp", "/proc/net/udp6"} var sockfiles = []string{"/proc/net/tcp", "/proc/net/tcp6", "/proc/net/udp", "/proc/net/udp6"}
var sawProcNetPermissionErr syncs.AtomicBool var sawProcNetPermissionErr atomic.Bool
const ( const (
v6Localhost = "00000000000000000000000001000000:" v6Localhost = "00000000000000000000000001000000:"
@ -37,7 +37,7 @@
) )
func listPorts() (List, error) { func listPorts() (List, error) {
if sawProcNetPermissionErr.Get() { if sawProcNetPermissionErr.Load() {
return nil, nil return nil, nil
} }
var ret []Port var ret []Port
@ -48,13 +48,13 @@ func listPorts() (List, error) {
// https://developer.android.com/about/versions/10/privacy/changes#proc-net-filesystem // https://developer.android.com/about/versions/10/privacy/changes#proc-net-filesystem
// Ignore it rather than have the system log about our violation. // Ignore it rather than have the system log about our violation.
if runtime.GOOS == "android" && syscall.Access(fname, unix.R_OK) != nil { if runtime.GOOS == "android" && syscall.Access(fname, unix.R_OK) != nil {
sawProcNetPermissionErr.Set(true) sawProcNetPermissionErr.Store(true)
return nil, nil return nil, nil
} }
f, err := os.Open(fname) f, err := os.Open(fname)
if os.IsPermission(err) { if os.IsPermission(err) {
sawProcNetPermissionErr.Set(true) sawProcNetPermissionErr.Store(true)
return nil, nil return nil, nil
} }
if err != nil { if err != nil {

View File

@ -12,11 +12,11 @@
"fmt" "fmt"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"testing" "testing"
"time" "time"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"tailscale.com/syncs"
"tailscale.com/tstest" "tailscale.com/tstest"
"tailscale.com/tsweb" "tailscale.com/tsweb"
) )
@ -131,10 +131,10 @@ func TestExpvar(t *testing.T) {
clk := newFakeTime() clk := newFakeTime()
p := newForTest(clk.Now, clk.NewTicker) p := newForTest(clk.Now, clk.NewTicker)
var succeed syncs.AtomicBool var succeed atomic.Bool
p.Run("probe", probeInterval, map[string]string{"label": "value"}, func(context.Context) error { p.Run("probe", probeInterval, map[string]string{"label": "value"}, func(context.Context) error {
clk.Advance(aFewMillis) clk.Advance(aFewMillis)
if succeed.Get() { if succeed.Load() {
return nil return nil
} }
return errors.New("failing, as instructed by test") return errors.New("failing, as instructed by test")
@ -172,7 +172,7 @@ func TestExpvar(t *testing.T) {
Result: false, Result: false,
}) })
succeed.Set(true) succeed.Store(true)
clk.Advance(probeInterval + halfProbeInterval) clk.Advance(probeInterval + halfProbeInterval)
st := epoch.Add(probeInterval + halfProbeInterval + aFewMillis) st := epoch.Add(probeInterval + halfProbeInterval + aFewMillis)
@ -189,10 +189,10 @@ func TestPrometheus(t *testing.T) {
clk := newFakeTime() clk := newFakeTime()
p := newForTest(clk.Now, clk.NewTicker) p := newForTest(clk.Now, clk.NewTicker)
var succeed syncs.AtomicBool var succeed atomic.Bool
p.Run("testprobe", probeInterval, map[string]string{"label": "value"}, func(context.Context) error { p.Run("testprobe", probeInterval, map[string]string{"label": "value"}, func(context.Context) error {
clk.Advance(aFewMillis) clk.Advance(aFewMillis)
if succeed.Get() { if succeed.Load() {
return nil return nil
} }
return errors.New("failing, as instructed by test") return errors.New("failing, as instructed by test")
@ -219,7 +219,7 @@ func TestPrometheus(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
succeed.Set(true) succeed.Store(true)
clk.Advance(probeInterval + halfProbeInterval) clk.Advance(probeInterval + halfProbeInterval)
err = tstest.WaitFor(convergenceTimeout, func() error { err = tstest.WaitFor(convergenceTimeout, func() error {

View File

@ -30,6 +30,7 @@
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
gossh "github.com/tailscale/golang-x-crypto/ssh" gossh "github.com/tailscale/golang-x-crypto/ssh"
@ -37,7 +38,6 @@
"tailscale.com/ipn/ipnlocal" "tailscale.com/ipn/ipnlocal"
"tailscale.com/logtail/backoff" "tailscale.com/logtail/backoff"
"tailscale.com/net/tsaddr" "tailscale.com/net/tsaddr"
"tailscale.com/syncs"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/tempfork/gliderlabs/ssh" "tailscale.com/tempfork/gliderlabs/ssh"
"tailscale.com/types/logger" "tailscale.com/types/logger"
@ -645,7 +645,7 @@ func (c *conn) resolveTerminalActionLocked(s ssh.Session, cr *contextReader) (ac
action = c.action0 action = c.action0
var awaitReadOnce sync.Once // to start Reads on cr var awaitReadOnce sync.Once // to start Reads on cr
var sawInterrupt syncs.AtomicBool var sawInterrupt atomic.Bool
var wg sync.WaitGroup var wg sync.WaitGroup
defer wg.Wait() // wait for awaitIntrOnce's goroutine to exit defer wg.Wait() // wait for awaitIntrOnce's goroutine to exit
@ -687,7 +687,7 @@ func (c *conn) resolveTerminalActionLocked(s ssh.Session, cr *contextReader) (ac
return return
} }
if n > 0 && buf[0] == 0x03 { // Ctrl-C if n > 0 && buf[0] == 0x03 { // Ctrl-C
sawInterrupt.Set(true) sawInterrupt.Store(true)
s.Stderr().Write([]byte("Canceled.\r\n")) s.Stderr().Write([]byte("Canceled.\r\n"))
s.Exit(1) s.Exit(1)
return return
@ -699,7 +699,7 @@ func (c *conn) resolveTerminalActionLocked(s ssh.Session, cr *contextReader) (ac
var err error var err error
action, err = c.fetchSSHAction(ctx, url) action, err = c.fetchSSHAction(ctx, url)
if err != nil { if err != nil {
if sawInterrupt.Get() { if sawInterrupt.Load() {
metricTerminalInterrupt.Add(1) metricTerminalInterrupt.Add(1)
return nil, fmt.Errorf("aborted by user") return nil, fmt.Errorf("aborted by user")
} else { } else {

View File

@ -68,42 +68,6 @@ func (wg *WaitGroupChan) Decr() {
// Wait blocks until the WaitGroupChan counter is zero. // Wait blocks until the WaitGroupChan counter is zero.
func (wg *WaitGroupChan) Wait() { <-wg.done } func (wg *WaitGroupChan) Wait() { <-wg.done }
// AtomicBool is an atomic boolean.
type AtomicBool int32
func (b *AtomicBool) Set(v bool) {
var n int32
if v {
n = 1
}
atomic.StoreInt32((*int32)(b), n)
}
// Swap sets b to v and reports whether it changed.
func (b *AtomicBool) Swap(v bool) (changed bool) {
var n int32
if v {
n = 1
}
old := atomic.SwapInt32((*int32)(b), n)
return old != n
}
func (b *AtomicBool) Get() bool {
return atomic.LoadInt32((*int32)(b)) != 0
}
// AtomicUint32 is an atomic uint32.
type AtomicUint32 uint32
func (b *AtomicUint32) Set(v uint32) {
atomic.StoreUint32((*uint32)(b), v)
}
func (b *AtomicUint32) Get() uint32 {
return atomic.LoadUint32((*uint32)(b))
}
// Semaphore is a counting semaphore. // Semaphore is a counting semaphore.
// //
// Use NewSemaphore to create one. // Use NewSemaphore to create one.

View File

@ -286,20 +286,20 @@ type Conn struct {
// is named negatively because in early start-up, we don't yet // is named negatively because in early start-up, we don't yet
// necessarily have a netcheck.Report and don't want to skip // necessarily have a netcheck.Report and don't want to skip
// logging. // logging.
noV4, noV6 syncs.AtomicBool noV4, noV6 atomic.Bool
// noV4Send is whether IPv4 UDP is known to be unable to transmit // noV4Send is whether IPv4 UDP is known to be unable to transmit
// at all. This could happen if the socket is in an invalid state // at all. This could happen if the socket is in an invalid state
// (as can happen on darwin after a network link status change). // (as can happen on darwin after a network link status change).
noV4Send syncs.AtomicBool noV4Send atomic.Bool
// networkUp is whether the network is up (some interface is up // networkUp is whether the network is up (some interface is up
// with IPv4 or IPv6). It's used to suppress log spam and prevent // with IPv4 or IPv6). It's used to suppress log spam and prevent
// new connection that'll fail. // new connection that'll fail.
networkUp syncs.AtomicBool networkUp atomic.Bool
// havePrivateKey is whether privateKey is non-zero. // havePrivateKey is whether privateKey is non-zero.
havePrivateKey syncs.AtomicBool havePrivateKey atomic.Bool
publicKeyAtomic atomic.Value // of key.NodePublic (or NodeKey zero value if !havePrivateKey) publicKeyAtomic atomic.Value // of key.NodePublic (or NodeKey zero value if !havePrivateKey)
// derpMapAtomic is the same as derpMap, but without requiring // derpMapAtomic is the same as derpMap, but without requiring
@ -310,7 +310,7 @@ type Conn struct {
lastNetCheckReport atomic.Pointer[netcheck.Report] lastNetCheckReport atomic.Pointer[netcheck.Report]
// port is the preferred port from opts.Port; 0 means auto. // port is the preferred port from opts.Port; 0 means auto.
port syncs.AtomicUint32 port atomic.Uint32
// ============================================================ // ============================================================
// mu guards all following fields; see userspaceEngine lock // mu guards all following fields; see userspaceEngine lock
@ -531,7 +531,7 @@ func newConn() *Conn {
} }
c.bind = &connBind{Conn: c, closed: true} c.bind = &connBind{Conn: c, closed: true}
c.muCond = sync.NewCond(&c.mu) c.muCond = sync.NewCond(&c.mu)
c.networkUp.Set(true) // assume up until told otherwise c.networkUp.Store(true) // assume up until told otherwise
return c return c
} }
@ -542,7 +542,7 @@ func newConn() *Conn {
// It doesn't start doing anything until Start is called. // It doesn't start doing anything until Start is called.
func NewConn(opts Options) (*Conn, error) { func NewConn(opts Options) (*Conn, error) {
c := newConn() c := newConn()
c.port.Set(uint32(opts.Port)) c.port.Store(uint32(opts.Port))
c.logf = opts.logf() c.logf = opts.logf()
c.epFunc = opts.endpointsFunc() c.epFunc = opts.endpointsFunc()
c.derpActiveFunc = opts.derpActiveFunc() c.derpActiveFunc = opts.derpActiveFunc()
@ -634,7 +634,7 @@ func (c *Conn) updateEndpoints(why string) {
c.muCond.Broadcast() c.muCond.Broadcast()
}() }()
c.logf("[v1] magicsock: starting endpoint update (%s)", why) c.logf("[v1] magicsock: starting endpoint update (%s)", why)
if c.noV4Send.Get() && runtime.GOOS != "js" { if c.noV4Send.Load() && runtime.GOOS != "js" {
c.mu.Lock() c.mu.Lock()
closed := c.closed closed := c.closed
c.mu.Unlock() c.mu.Unlock()
@ -736,9 +736,9 @@ func (c *Conn) updateNetInfo(ctx context.Context) (*netcheck.Report, error) {
} }
c.lastNetCheckReport.Store(report) c.lastNetCheckReport.Store(report)
c.noV4.Set(!report.IPv4) c.noV4.Store(!report.IPv4)
c.noV6.Set(!report.IPv6) c.noV6.Store(!report.IPv6)
c.noV4Send.Set(!report.IPv4CanSend) c.noV4Send.Store(!report.IPv4CanSend)
ni := &tailcfg.NetInfo{ ni := &tailcfg.NetInfo{
DERPLatency: map[string]float64{}, DERPLatency: map[string]float64{},
@ -1074,7 +1074,7 @@ func (c *Conn) determineEndpoints(ctx context.Context) ([]tailcfg.Endpoint, erro
// port mapping on their router to the same explicit // port mapping on their router to the same explicit
// port that tailscaled is running with. Worst case // port that tailscaled is running with. Worst case
// it's an invalid candidate mapping. // it's an invalid candidate mapping.
if port := c.port.Get(); nr.MappingVariesByDestIP.EqualBool(true) && port != 0 { if port := c.port.Load(); nr.MappingVariesByDestIP.EqualBool(true) && port != 0 {
if ip, _, err := net.SplitHostPort(nr.GlobalV4); err == nil { if ip, _, err := net.SplitHostPort(nr.GlobalV4); err == nil {
addAddr(ipp(net.JoinHostPort(ip, strconv.Itoa(int(port)))), tailcfg.EndpointSTUN4LocalPort) addAddr(ipp(net.JoinHostPort(ip, strconv.Itoa(int(port)))), tailcfg.EndpointSTUN4LocalPort)
} }
@ -1167,7 +1167,7 @@ func (c *Conn) LocalPort() uint16 {
var errNetworkDown = errors.New("magicsock: network down") var errNetworkDown = errors.New("magicsock: network down")
func (c *Conn) networkDown() bool { return !c.networkUp.Get() } func (c *Conn) networkDown() bool { return !c.networkUp.Load() }
func (c *Conn) Send(b []byte, ep conn.Endpoint) error { func (c *Conn) Send(b []byte, ep conn.Endpoint) error {
metricSendData.Add(1) metricSendData.Add(1)
@ -1207,7 +1207,7 @@ func (c *Conn) sendUDPStd(addr netip.AddrPort, b []byte) (sent bool, err error)
switch { switch {
case addr.Addr().Is4(): case addr.Addr().Is4():
_, err = c.pconn4.WriteToUDPAddrPort(b, addr) _, err = c.pconn4.WriteToUDPAddrPort(b, addr)
if err != nil && (c.noV4.Get() || neterror.TreatAsLostUDP(err)) { if err != nil && (c.noV4.Load() || neterror.TreatAsLostUDP(err)) {
return false, nil return false, nil
} }
case addr.Addr().Is6(): case addr.Addr().Is6():
@ -1216,7 +1216,7 @@ func (c *Conn) sendUDPStd(addr netip.AddrPort, b []byte) (sent bool, err error)
return false, nil return false, nil
} }
_, err = c.pconn6.WriteToUDPAddrPort(b, addr) _, err = c.pconn6.WriteToUDPAddrPort(b, addr)
if err != nil && (c.noV6.Get() || neterror.TreatAsLostUDP(err)) { if err != nil && (c.noV6.Load() || neterror.TreatAsLostUDP(err)) {
return false, nil return false, nil
} }
default: default:
@ -1674,7 +1674,7 @@ func (c *Conn) receiveIP(b []byte, ipp netip.AddrPort, cache *ippEndpointCache)
if c.handleDiscoMessage(b, ipp, key.NodePublic{}) { if c.handleDiscoMessage(b, ipp, key.NodePublic{}) {
return nil, false return nil, false
} }
if !c.havePrivateKey.Get() { if !c.havePrivateKey.Load() {
// If we have no private key, we're logged out or // If we have no private key, we're logged out or
// stopped. Don't try to pass these wireguard packets // stopped. Don't try to pass these wireguard packets
// up to wireguard-go; it'll just complain (issue 1167). // up to wireguard-go; it'll just complain (issue 1167).
@ -2140,12 +2140,12 @@ func (c *Conn) discoInfoLocked(k key.DiscoPublic) *discoInfo {
func (c *Conn) SetNetworkUp(up bool) { func (c *Conn) SetNetworkUp(up bool) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
if c.networkUp.Get() == up { if c.networkUp.Load() == up {
return return
} }
c.logf("magicsock: SetNetworkUp(%v)", up) c.logf("magicsock: SetNetworkUp(%v)", up)
c.networkUp.Set(up) c.networkUp.Store(up)
if up { if up {
c.startDerpHomeConnectLocked() c.startDerpHomeConnectLocked()
@ -2157,10 +2157,10 @@ func (c *Conn) SetNetworkUp(up bool) {
// SetPreferredPort sets the connection's preferred local port. // SetPreferredPort sets the connection's preferred local port.
func (c *Conn) SetPreferredPort(port uint16) { func (c *Conn) SetPreferredPort(port uint16) {
if uint16(c.port.Get()) == port { if uint16(c.port.Load()) == port {
return return
} }
c.port.Set(uint32(port)) c.port.Store(uint32(port))
if err := c.rebind(dropCurrentPort); err != nil { if err := c.rebind(dropCurrentPort); err != nil {
c.logf("%w", err) c.logf("%w", err)
@ -2185,7 +2185,7 @@ func (c *Conn) SetPrivateKey(privateKey key.NodePrivate) error {
return nil return nil
} }
c.privateKey = newKey c.privateKey = newKey
c.havePrivateKey.Set(!newKey.IsZero()) c.havePrivateKey.Store(!newKey.IsZero())
if newKey.IsZero() { if newKey.IsZero() {
c.publicKeyAtomic.Store(key.NodePublic{}) c.publicKeyAtomic.Store(key.NodePublic{})
@ -2835,7 +2835,7 @@ func (c *Conn) bindSocket(rucPtr **RebindingUDPConn, network string, curPortFate
// Second best is the port that is currently in use. // Second best is the port that is currently in use.
// If those fail, fall back to 0. // If those fail, fall back to 0.
var ports []uint16 var ports []uint16
if port := uint16(c.port.Get()); port != 0 { if port := uint16(c.port.Load()); port != 0 {
ports = append(ports, port) ports = append(ports, port)
} }
if ruc.pconn != nil && curPortFate == keepCurrentPort { if ruc.pconn != nil && curPortFate == keepCurrentPort {

View File

@ -14,6 +14,7 @@
"os/exec" "os/exec"
"strconv" "strconv"
"strings" "strings"
"sync/atomic"
"syscall" "syscall"
"time" "time"
@ -25,7 +26,6 @@
"golang.zx2c4.com/wireguard/tun" "golang.zx2c4.com/wireguard/tun"
"tailscale.com/envknob" "tailscale.com/envknob"
"tailscale.com/net/tsaddr" "tailscale.com/net/tsaddr"
"tailscale.com/syncs"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/types/preftype" "tailscale.com/types/preftype"
"tailscale.com/util/multierr" "tailscale.com/util/multierr"
@ -84,7 +84,7 @@ type netfilterRunner interface {
} }
type linuxRouter struct { type linuxRouter struct {
closed syncs.AtomicBool closed atomic.Bool
logf func(fmt string, args ...any) logf func(fmt string, args ...any)
tunname string tunname string
linkMon *monitor.Mon linkMon *monitor.Mon
@ -97,7 +97,7 @@ type linuxRouter struct {
// ruleRestorePending is whether a timer has been started to // ruleRestorePending is whether a timer has been started to
// restore deleted ip rules. // restore deleted ip rules.
ruleRestorePending syncs.AtomicBool ruleRestorePending atomic.Bool
ipRuleFixLimiter *rate.Limiter ipRuleFixLimiter *rate.Limiter
// Various feature checks for the network stack. // Various feature checks for the network stack.
@ -233,7 +233,7 @@ func (r *linuxRouter) onIPRuleDeleted(table uint8, priority uint32) {
return return
} }
time.AfterFunc(rr.Delay()+250*time.Millisecond, func() { time.AfterFunc(rr.Delay()+250*time.Millisecond, func() {
if r.ruleRestorePending.Swap(false) && !r.closed.Get() { if r.ruleRestorePending.Swap(false) && !r.closed.Load() {
r.logf("somebody (likely systemd-networkd) deleted ip rules; restoring Tailscale's") r.logf("somebody (likely systemd-networkd) deleted ip rules; restoring Tailscale's")
r.justAddIPRules() r.justAddIPRules()
} }
@ -258,7 +258,7 @@ func (r *linuxRouter) Up() error {
} }
func (r *linuxRouter) Close() error { func (r *linuxRouter) Close() error {
r.closed.Set(true) r.closed.Store(true)
if r.unregLinkMon != nil { if r.unregLinkMon != nil {
r.unregLinkMon() r.unregLinkMon()
} }