wgengine: lazily create peer wireguard configs more explicitly

Rather than consider bigs jumps in last-received-from activity as a
signal to possibly reconfigure the set of wireguard peers to have
configured, instead just track the set of peers that are currently
excluded from the configuration. Easier to reason about.

Also adds a bit more logging.

This might fix an error we saw on a machine running a recent unstable
build:

2020-08-26 17:54:11.528033751 +0000 UTC: 8.6M/92.6M magicsock: [unexpected] lazy endpoint not created for [UcppE], d:42a770f678357249
2020-08-26 17:54:13.691305296 +0000 UTC: 8.7M/92.6M magicsock: DERP packet received from idle peer [UcppE]; created=false
2020-08-26 17:54:13.691383687 +0000 UTC: 8.7M/92.6M magicsock: DERP packet from unknown key: [UcppE]

If it does happen again, though, we'll have more logs.
This commit is contained in:
Brad Fitzpatrick 2020-08-26 12:20:09 -07:00
parent 483141094c
commit 756d6a72bd
3 changed files with 30 additions and 28 deletions

View File

@ -1765,6 +1765,7 @@ func (c *Conn) handleDiscoMessage(msg []byte, src netaddr.IPPort) bool {
needsRecvActivityCall = de.isFirstRecvActivityInAwhile() needsRecvActivityCall = de.isFirstRecvActivityInAwhile()
} }
if needsRecvActivityCall && c.noteRecvActivity != nil { if needsRecvActivityCall && c.noteRecvActivity != nil {
c.logf("magicsock: got disco message from idle peer, starting lazy conf for %v, %v", peerNode.Key.ShortString(), sender.ShortString())
// We can't hold Conn.mu while calling noteRecvActivity. // We can't hold Conn.mu while calling noteRecvActivity.
// noteRecvActivity acquires userspaceEngine.wgLock (and per our // noteRecvActivity acquires userspaceEngine.wgLock (and per our
// lock ordering rules: wgLock must come first), and also calls // lock ordering rules: wgLock must come first), and also calls

View File

@ -105,7 +105,8 @@ type userspaceEngine struct {
lastEngineSigFull string // of full wireguard config lastEngineSigFull string // of full wireguard config
lastEngineSigTrim string // of trimmed wireguard config lastEngineSigTrim string // of trimmed wireguard config
recvActivityAt map[tailcfg.DiscoKey]time.Time recvActivityAt map[tailcfg.DiscoKey]time.Time
sentActivityAt map[packet.IP]*int64 // value is atomic int64 of unixtime trimmedDisco map[tailcfg.DiscoKey]bool // set of disco keys of peers currently excluded from wireguard config
sentActivityAt map[packet.IP]*int64 // value is atomic int64 of unixtime
destIPActivityFuncs map[packet.IP]func() destIPActivityFuncs map[packet.IP]func()
mu sync.Mutex // guards following; see lock order comment below mu sync.Mutex // guards following; see lock order comment below
@ -636,9 +637,11 @@ func (e *userspaceEngine) noteReceiveActivity(dk tailcfg.DiscoKey) {
e.wgLock.Lock() e.wgLock.Lock()
defer e.wgLock.Unlock() defer e.wgLock.Unlock()
was, ok := e.recvActivityAt[dk] if _, ok := e.recvActivityAt[dk]; !ok {
if !ok {
// Not a trimmable peer we care about tracking. (See isTrimmablePeer) // Not a trimmable peer we care about tracking. (See isTrimmablePeer)
if e.trimmedDisco[dk] {
e.logf("wgengine: [unexpected] noteReceiveActivity called on idle discokey %v that's not in recvActivityAt", dk.ShortString())
}
return return
} }
now := e.timeNow() now := e.timeNow()
@ -650,7 +653,8 @@ func (e *userspaceEngine) noteReceiveActivity(dk tailcfg.DiscoKey) {
// lazyPeerIdleThreshold without the divide by 2, but // lazyPeerIdleThreshold without the divide by 2, but
// maybeReconfigWireguardLocked is cheap enough to call every // maybeReconfigWireguardLocked is cheap enough to call every
// couple minutes (just not on every packet). // couple minutes (just not on every packet).
if was.IsZero() || now.Sub(was) > lazyPeerIdleThreshold/2 { if e.trimmedDisco[dk] {
e.logf("wgengine: idle peer %v now active, reconfiguring wireguard", dk.ShortString())
e.maybeReconfigWireguardLocked() e.maybeReconfigWireguardLocked()
} }
} }
@ -718,6 +722,8 @@ func (e *userspaceEngine) maybeReconfigWireguardLocked() error {
trackDisco := make([]tailcfg.DiscoKey, 0, len(full.Peers)) trackDisco := make([]tailcfg.DiscoKey, 0, len(full.Peers))
trackIPs := make([]wgcfg.IP, 0, len(full.Peers)) trackIPs := make([]wgcfg.IP, 0, len(full.Peers))
trimmedDisco := map[tailcfg.DiscoKey]bool{} // TODO: don't re-alloc this map each time
for i := range full.Peers { for i := range full.Peers {
p := &full.Peers[i] p := &full.Peers[i]
if !isTrimmablePeer(p, len(full.Peers)) { if !isTrimmablePeer(p, len(full.Peers)) {
@ -730,6 +736,8 @@ func (e *userspaceEngine) maybeReconfigWireguardLocked() error {
trackIPs = append(trackIPs, tsIP) trackIPs = append(trackIPs, tsIP)
if e.isActiveSince(dk, tsIP, activeCutoff) { if e.isActiveSince(dk, tsIP, activeCutoff) {
min.Peers = append(min.Peers, *p) min.Peers = append(min.Peers, *p)
} else {
trimmedDisco[dk] = true
} }
} }
@ -738,6 +746,8 @@ func (e *userspaceEngine) maybeReconfigWireguardLocked() error {
return nil return nil
} }
e.trimmedDisco = trimmedDisco
e.updateActivityMapsLocked(trackDisco, trackIPs) e.updateActivityMapsLocked(trackDisco, trackIPs)
e.logf("wgengine: Reconfig: configuring userspace wireguard config (with %d/%d peers)", len(min.Peers), len(full.Peers)) e.logf("wgengine: Reconfig: configuring userspace wireguard config (with %d/%d peers)", len(min.Peers), len(full.Peers))

View File

@ -17,7 +17,6 @@
func TestNoteReceiveActivity(t *testing.T) { func TestNoteReceiveActivity(t *testing.T) {
now := time.Unix(1, 0) now := time.Unix(1, 0)
tick := func(d time.Duration) { now = now.Add(d) }
var logBuf bytes.Buffer var logBuf bytes.Buffer
confc := make(chan bool, 1) confc := make(chan bool, 1)
@ -37,6 +36,7 @@ func TestNoteReceiveActivity(t *testing.T) {
}, },
tundev: new(tstun.TUN), tundev: new(tstun.TUN),
testMaybeReconfigHook: func() { confc <- true }, testMaybeReconfigHook: func() { confc <- true },
trimmedDisco: map[tailcfg.DiscoKey]bool{},
} }
ra := e.recvActivityAt ra := e.recvActivityAt
@ -51,7 +51,7 @@ func TestNoteReceiveActivity(t *testing.T) {
t.Fatalf("unexpected log write (and thus activity): %s", logBuf.Bytes()) t.Fatalf("unexpected log write (and thus activity): %s", logBuf.Bytes())
} }
// Now track it and expect updates. // Now track it, but don't mark it trimmed, so shouldn't update.
ra[dk] = time.Time{} ra[dk] = time.Time{}
e.noteReceiveActivity(dk) e.noteReceiveActivity(dk)
if len(ra) != 1 { if len(ra) != 1 {
@ -60,29 +60,20 @@ func TestNoteReceiveActivity(t *testing.T) {
if got := ra[dk]; got != now { if got := ra[dk]; got != now {
t.Fatalf("time in map = %v; want %v", got, now) t.Fatalf("time in map = %v; want %v", got, now)
} }
if gotConf() {
t.Fatalf("unexpected reconfig")
}
// Now mark it trimmed and expect an update.
e.trimmedDisco[dk] = true
e.noteReceiveActivity(dk)
if len(ra) != 1 {
t.Fatalf("unexpected growth in map: now has %d keys; want 1", len(ra))
}
if got := ra[dk]; got != now {
t.Fatalf("time in map = %v; want %v", got, now)
}
if !gotConf() { if !gotConf() {
t.Fatalf("didn't get expected reconfig") t.Fatalf("didn't get expected reconfig")
} }
// With updates 1 second apart, don't expect a reconfig.
for i := 0; i < 300; i++ {
tick(time.Second)
e.noteReceiveActivity(dk)
if len(ra) != 1 {
t.Fatalf("map len = %d; want 1", len(ra))
}
if got := ra[dk]; got != now {
t.Fatalf("time in map = %v; want %v", got, now)
}
if gotConf() {
t.Fatalf("unexpected reconfig")
}
}
// But if there's a big jump it should get an update.
tick(3 * time.Minute)
e.noteReceiveActivity(dk)
if !gotConf() {
t.Fatalf("expected config")
}
} }