| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016 |
- // Copyright (c) Tailscale Inc & AUTHORS
- // SPDX-License-Identifier: BSD-3-Clause
- package ipnlocal
- import (
- "context"
- "errors"
- "fmt"
- "math/rand/v2"
- "net/netip"
- "strings"
- "sync"
- "sync/atomic"
- "testing"
- "time"
- qt "github.com/frankban/quicktest"
- "github.com/google/go-cmp/cmp"
- "github.com/google/go-cmp/cmp/cmpopts"
- "tailscale.com/control/controlclient"
- "tailscale.com/envknob"
- "tailscale.com/ipn"
- "tailscale.com/ipn/ipnauth"
- "tailscale.com/ipn/ipnstate"
- "tailscale.com/ipn/store/mem"
- "tailscale.com/net/dns"
- "tailscale.com/net/netmon"
- "tailscale.com/net/packet"
- "tailscale.com/net/tsdial"
- "tailscale.com/tailcfg"
- "tailscale.com/tsd"
- "tailscale.com/tstest"
- "tailscale.com/types/dnstype"
- "tailscale.com/types/key"
- "tailscale.com/types/logger"
- "tailscale.com/types/logid"
- "tailscale.com/types/netmap"
- "tailscale.com/types/persist"
- "tailscale.com/types/preftype"
- "tailscale.com/util/dnsname"
- "tailscale.com/util/eventbus/eventbustest"
- "tailscale.com/util/mak"
- "tailscale.com/util/must"
- "tailscale.com/wgengine"
- "tailscale.com/wgengine/filter"
- "tailscale.com/wgengine/magicsock"
- "tailscale.com/wgengine/router"
- "tailscale.com/wgengine/wgcfg"
- "tailscale.com/wgengine/wgint"
- )
- // notifyThrottler receives notifications from an ipn.Backend, blocking
- // (with eventual timeout and t.Fatal) if there are too many and complaining
- // (also with t.Fatal) if they are too few.
- type notifyThrottler struct {
- t *testing.T
- // ch gets replaced frequently. Lock the mutex before getting or
- // setting it, but not while waiting on it.
- mu sync.Mutex
- ch chan ipn.Notify
- putErr error // set by put if the channel is full
- }
- // expect tells the throttler to expect count upcoming notifications.
- func (nt *notifyThrottler) expect(count int) {
- nt.mu.Lock()
- nt.ch = make(chan ipn.Notify, count)
- nt.mu.Unlock()
- }
- // put adds one notification into the throttler's queue.
- func (nt *notifyThrottler) put(n ipn.Notify) {
- nt.t.Helper()
- nt.mu.Lock()
- ch := nt.ch
- nt.mu.Unlock()
- select {
- case ch <- n:
- return
- default:
- err := fmt.Errorf("put: channel full: %v", n)
- nt.t.Log(err)
- nt.mu.Lock()
- nt.putErr = err
- nt.mu.Unlock()
- }
- }
- // drain pulls the notifications out of the queue, asserting that there are
- // exactly count notifications that have been put so far.
- func (nt *notifyThrottler) drain(count int) []ipn.Notify {
- nt.t.Helper()
- nt.mu.Lock()
- ch := nt.ch
- putErr := nt.putErr
- nt.mu.Unlock()
- if putErr != nil {
- nt.t.Fatalf("drain: previous call to put errored: %s", putErr)
- }
- nn := []ipn.Notify{}
- for i := range count {
- select {
- case n := <-ch:
- nn = append(nn, n)
- case <-time.After(6 * time.Second):
- nt.t.Fatalf("drain: channel empty after %d/%d", i, count)
- }
- }
- // no more notifications expected
- close(ch)
- nt.t.Log(nn)
- return nn
- }
- // mockControl is a mock implementation of controlclient.Client.
- // Much of the backend state machine depends on callbacks and state
- // in the controlclient.Client, so by controlling it, we can check that
- // the state machine works as expected.
- type mockControl struct {
- tb testing.TB
- logf logger.Logf
- opts controlclient.Options
- paused atomic.Bool
- controlClientID int64
- mu sync.Mutex
- persist *persist.Persist
- calls []string
- authBlocked bool
- shutdown chan struct{}
- }
- func newClient(tb testing.TB, opts controlclient.Options) *mockControl {
- return &mockControl{
- tb: tb,
- authBlocked: true,
- logf: opts.Logf,
- opts: opts,
- shutdown: make(chan struct{}),
- persist: opts.Persist.Clone(),
- controlClientID: rand.Int64(),
- }
- }
- func (cc *mockControl) assertShutdown(wasPaused bool) {
- cc.tb.Helper()
- select {
- case <-cc.shutdown:
- // ok
- case <-time.After(500 * time.Millisecond):
- cc.tb.Fatalf("timed out waiting for shutdown")
- }
- if wasPaused {
- cc.assertCalls("unpause", "Shutdown")
- } else {
- cc.assertCalls("Shutdown")
- }
- }
- func (cc *mockControl) populateKeys() (newKeys bool) {
- cc.mu.Lock()
- defer cc.mu.Unlock()
- if cc.persist == nil {
- cc.persist = &persist.Persist{}
- }
- if cc.persist != nil && cc.persist.PrivateNodeKey.IsZero() {
- cc.logf("Generating a new nodekey.")
- cc.persist.OldPrivateNodeKey = cc.persist.PrivateNodeKey
- cc.persist.PrivateNodeKey = key.NewNode()
- newKeys = true
- }
- return newKeys
- }
- type sendOpt struct {
- err error
- url string
- loginFinished bool
- nm *netmap.NetworkMap
- }
- // send publishes a controlclient.Status notification upstream.
- // (In our tests here, upstream is the ipnlocal.Local instance.)
- func (cc *mockControl) send(opts sendOpt) {
- err, url, loginFinished, nm := opts.err, opts.url, opts.loginFinished, opts.nm
- if loginFinished {
- cc.mu.Lock()
- cc.authBlocked = false
- cc.mu.Unlock()
- }
- if cc.opts.Observer != nil {
- s := controlclient.Status{
- URL: url,
- NetMap: nm,
- Persist: cc.persist.View(),
- Err: err,
- }
- if loginFinished {
- s.LoggedIn = true
- }
- cc.opts.Observer.SetControlClientStatus(cc, s)
- }
- }
- func (cc *mockControl) authenticated(nm *netmap.NetworkMap) {
- if selfUser, ok := nm.UserProfiles[nm.SelfNode.User()]; ok {
- cc.persist.UserProfile = *selfUser.AsStruct()
- }
- cc.persist.NodeID = nm.SelfNode.StableID()
- cc.send(sendOpt{loginFinished: true, nm: nm})
- }
- func (cc *mockControl) sendAuthURL(nm *netmap.NetworkMap) {
- s := controlclient.Status{
- URL: "https://example.com/a/foo",
- NetMap: nm,
- Persist: cc.persist.View(),
- }
- cc.opts.Observer.SetControlClientStatus(cc, s)
- }
- // called records that a particular function name was called.
- func (cc *mockControl) called(s string) {
- cc.mu.Lock()
- defer cc.mu.Unlock()
- cc.calls = append(cc.calls, s)
- }
- // assertCalls fails the test if the list of functions that have been called since the
- // last time assertCall was run does not match want.
- func (cc *mockControl) assertCalls(want ...string) {
- cc.tb.Helper()
- cc.mu.Lock()
- defer cc.mu.Unlock()
- qt.Assert(cc.tb, cc.calls, qt.DeepEquals, want)
- cc.calls = nil
- }
- // Shutdown disconnects the client.
- func (cc *mockControl) Shutdown() {
- cc.logf("Shutdown")
- cc.called("Shutdown")
- close(cc.shutdown)
- }
- // Login starts a login process. Note that in this mock, we don't automatically
- // generate notifications about the progress of the login operation. You have to
- // call send() as required by the test.
- func (cc *mockControl) Login(flags controlclient.LoginFlags) {
- cc.logf("Login flags=%v", flags)
- cc.called("Login")
- newKeys := cc.populateKeys()
- interact := (flags & controlclient.LoginInteractive) != 0
- cc.logf("Login: interact=%v newKeys=%v", interact, newKeys)
- cc.mu.Lock()
- defer cc.mu.Unlock()
- cc.authBlocked = interact || newKeys
- }
- func (cc *mockControl) Logout(ctx context.Context) error {
- cc.logf("Logout")
- cc.called("Logout")
- return nil
- }
- func (cc *mockControl) SetPaused(paused bool) {
- was := cc.paused.Swap(paused)
- if was == paused {
- return
- }
- cc.logf("SetPaused=%v", paused)
- if paused {
- cc.called("pause")
- } else {
- cc.called("unpause")
- }
- }
- func (cc *mockControl) AuthCantContinue() bool {
- cc.mu.Lock()
- defer cc.mu.Unlock()
- return cc.authBlocked
- }
- func (cc *mockControl) SetHostinfo(hi *tailcfg.Hostinfo) {
- cc.logf("SetHostinfo: %v", *hi)
- cc.called("SetHostinfo")
- }
- func (cc *mockControl) SetNetInfo(ni *tailcfg.NetInfo) {
- cc.called("SetNetinfo")
- cc.logf("SetNetInfo: %v", *ni)
- cc.called("SetNetInfo")
- }
- func (cc *mockControl) SetTKAHead(head string) {
- cc.logf("SetTKAHead: %s", head)
- }
- func (cc *mockControl) UpdateEndpoints(endpoints []tailcfg.Endpoint) {
- // validate endpoint information here?
- cc.logf("UpdateEndpoints: ep=%v", endpoints)
- cc.called("UpdateEndpoints")
- }
- func (cc *mockControl) SetDiscoPublicKey(key key.DiscoPublic) {
- cc.logf("SetDiscoPublicKey: %v", key)
- cc.called("SetDiscoPublicKey")
- }
- func (cc *mockControl) ClientID() int64 {
- return cc.controlClientID
- }
- func (b *LocalBackend) nonInteractiveLoginForStateTest() {
- b.mu.Lock()
- if b.cc == nil {
- panic("LocalBackend.assertClient: b.cc == nil")
- }
- cc := b.cc
- b.mu.Unlock()
- cc.Login(b.loginFlags | controlclient.LoginInteractive)
- }
- // A very precise test of the sequence of function calls generated by
- // ipnlocal.Local into its controlclient instance, and the events it
- // produces upstream into the UI.
- //
- // [apenwarr] Normally I'm not a fan of "mock" style tests, but the precise
- // sequence of this state machine is so important for writing our multiple
- // frontends, that it's worth validating it all in one place.
- //
- // Any changes that affect this test will most likely require carefully
- // re-testing all our GUIs (and the CLI) to make sure we didn't break
- // anything.
- //
- // Note also that this test doesn't have any timers, goroutines, or duplicate
- // detection. It expects messages to be produced in exactly the right order,
- // with no duplicates, without doing network activity (other than through
- // controlclient, which we fake, so there's no network activity there either).
- //
- // TODO: A few messages that depend on magicsock (which actually might have
- // network delays) are just ignored for now, which makes the test
- // predictable, but maybe a bit less thorough. This is more of an overall
- // state machine test than a test of the wgengine+magicsock integration.
- func TestStateMachine(t *testing.T) {
- runTestStateMachine(t, false)
- }
- func TestStateMachineSeamless(t *testing.T) {
- runTestStateMachine(t, true)
- }
- func runTestStateMachine(t *testing.T, seamless bool) {
- envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1")
- defer envknob.Setenv("TAILSCALE_USE_WIP_CODE", "")
- c := qt.New(t)
- logf := tstest.WhileTestRunningLogger(t)
- sys := tsd.NewSystem()
- store := new(testStateStorage)
- sys.Set(store)
- e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker.Get(), sys.UserMetricsRegistry(), sys.Bus.Get())
- if err != nil {
- t.Fatalf("NewFakeUserspaceEngine: %v", err)
- }
- t.Cleanup(e.Close)
- sys.Set(e)
- b, err := NewLocalBackend(logf, logid.PublicID{}, sys, 0)
- if err != nil {
- t.Fatalf("NewLocalBackend: %v", err)
- }
- t.Cleanup(b.Shutdown)
- var cc, previousCC *mockControl
- b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) {
- previousCC = cc
- cc = newClient(t, opts)
- t.Logf("ccGen: new mockControl.")
- cc.called("New")
- return cc, nil
- })
- notifies := ¬ifyThrottler{t: t}
- notifies.expect(0)
- b.SetNotifyCallback(func(n ipn.Notify) {
- if n.State != nil ||
- (n.Prefs != nil && n.Prefs.Valid()) ||
- n.BrowseToURL != nil ||
- n.LoginFinished != nil {
- logf("%+v\n\n", n)
- notifies.put(n)
- } else {
- logf("(ignored) %v\n\n", n)
- }
- })
- // Check that it hasn't called us right away.
- // The state machine should be idle until we call Start().
- c.Assert(cc, qt.IsNil)
- // Start the state machine.
- // Since !WantRunning by default, it'll create a controlclient,
- // but not ask it to do anything yet.
- t.Logf("\n\nStart")
- notifies.expect(2)
- c.Assert(b.Start(ipn.Options{}), qt.IsNil)
- {
- // BUG: strictly, it should pause, not unpause, here, since !WantRunning.
- cc.assertCalls("New")
- nn := notifies.drain(2)
- cc.assertCalls()
- c.Assert(nn[0].Prefs, qt.IsNotNil)
- c.Assert(nn[1].State, qt.IsNotNil)
- prefs := nn[0].Prefs
- // Note: a totally fresh system has Prefs.LoggedOut=false by
- // default. We are logged out, but not because the user asked
- // for it, so it doesn't count as Prefs.LoggedOut==true.
- c.Assert(prefs.LoggedOut(), qt.IsTrue)
- c.Assert(prefs.WantRunning(), qt.IsFalse)
- // Verify notification indicates we need login (prefs show logged out)
- c.Assert(nn[1].Prefs == nil || nn[1].Prefs.LoggedOut(), qt.IsTrue)
- // Verify the actual facts about our state
- c.Assert(needsLogin(b), qt.IsTrue)
- c.Assert(hasValidNetMap(b), qt.IsFalse)
- }
- // Restart the state machine.
- // It's designed to handle frontends coming and going sporadically.
- // Make the sure the restart not only works, but generates the same
- // events as the first time, so UIs always know what to expect.
- t.Logf("\n\nStart2")
- notifies.expect(2)
- c.Assert(b.Start(ipn.Options{}), qt.IsNil)
- {
- previousCC.assertShutdown(false)
- cc.assertCalls("New")
- nn := notifies.drain(2)
- cc.assertCalls()
- c.Assert(nn[0].Prefs, qt.IsNotNil)
- c.Assert(nn[1].State, qt.IsNotNil)
- c.Assert(nn[0].Prefs.LoggedOut(), qt.IsTrue)
- c.Assert(nn[0].Prefs.WantRunning(), qt.IsFalse)
- // Verify notification indicates we need login
- c.Assert(nn[1].Prefs == nil || nn[1].Prefs.LoggedOut(), qt.IsTrue)
- // Verify the actual facts about our state
- c.Assert(needsLogin(b), qt.IsTrue)
- c.Assert(hasValidNetMap(b), qt.IsFalse)
- }
- // Start non-interactive login with no token.
- // This will ask controlclient to start its own Login() process,
- // then wait for us to respond.
- t.Logf("\n\nLogin (noninteractive)")
- notifies.expect(0)
- b.nonInteractiveLoginForStateTest()
- {
- cc.assertCalls("Login")
- notifies.drain(0)
- // Note: WantRunning isn't true yet. It'll switch to true
- // after a successful login finishes.
- // (This behaviour is needed so that b.Login() won't
- // start connecting to an old account right away, if one
- // exists when you launch another login.)
- // Verify we still need login
- c.Assert(needsLogin(b), qt.IsTrue)
- }
- // Attempted non-interactive login with no key; indicate that
- // the user needs to visit a login URL.
- t.Logf("\n\nLogin (url response)")
- notifies.expect(3)
- b.EditPrefs(&ipn.MaskedPrefs{
- ControlURLSet: true,
- Prefs: ipn.Prefs{
- ControlURL: "https://localhost:1/",
- },
- })
- url1 := "https://localhost:1/1"
- cc.send(sendOpt{url: url1})
- {
- cc.assertCalls()
- // ...but backend eats that notification, because the user
- // didn't explicitly request interactive login yet, and
- // we're already in NeedsLogin state.
- nn := notifies.drain(3)
- c.Assert(nn[1].Prefs, qt.IsNotNil)
- c.Assert(nn[1].Prefs.LoggedOut(), qt.IsTrue)
- c.Assert(nn[1].Prefs.WantRunning(), qt.IsFalse)
- // Verify we need URL visit
- c.Assert(hasAuthURL(b), qt.IsTrue)
- c.Assert(nn[2].BrowseToURL, qt.IsNotNil)
- c.Assert(url1, qt.Equals, *nn[2].BrowseToURL)
- c.Assert(isFullyAuthenticated(b), qt.IsFalse)
- }
- // Now we'll try an interactive login.
- // Since we provided an interactive URL earlier, this shouldn't
- // ask control to do anything. Instead backend will emit an event
- // indicating that the UI should browse to the given URL.
- t.Logf("\n\nLogin (interactive)")
- notifies.expect(1)
- b.StartLoginInteractive(context.Background())
- {
- nn := notifies.drain(1)
- cc.assertCalls()
- c.Assert(nn[0].BrowseToURL, qt.IsNotNil)
- c.Assert(url1, qt.Equals, *nn[0].BrowseToURL)
- // Verify we still need to complete login
- c.Assert(needsLogin(b), qt.IsTrue)
- }
- // Sometimes users press the Login button again, in the middle of
- // a login sequence. For example, they might have closed their
- // browser window without logging in, or they waited too long and
- // the login URL expired. If they start another interactive login,
- // we must always get a *new* login URL first.
- t.Logf("\n\nLogin2 (interactive)")
- b.authURLTime = time.Now().Add(-time.Hour * 24 * 7) // simulate URL expiration
- notifies.expect(0)
- b.StartLoginInteractive(context.Background())
- {
- notifies.drain(0)
- // backend asks control for another login sequence
- cc.assertCalls("Login")
- // Verify we still need login
- c.Assert(needsLogin(b), qt.IsTrue)
- }
- // Provide a new interactive login URL.
- t.Logf("\n\nLogin2 (url response)")
- notifies.expect(1)
- url2 := "https://localhost:1/2"
- cc.send(sendOpt{url: url2})
- {
- cc.assertCalls()
- // This time, backend should emit it to the UI right away,
- // because the UI is anxiously awaiting a new URL to visit.
- nn := notifies.drain(1)
- c.Assert(nn[0].BrowseToURL, qt.IsNotNil)
- c.Assert(url2, qt.Equals, *nn[0].BrowseToURL)
- // Verify we still need to complete login
- c.Assert(needsLogin(b), qt.IsTrue)
- }
- // Pretend that the interactive login actually happened.
- // Controlclient always sends the netmap and LoginFinished at the
- // same time.
- // The backend should propagate this upward for the UI.
- t.Logf("\n\nLoginFinished")
- notifies.expect(3)
- cc.persist.UserProfile.LoginName = "user1"
- cc.persist.NodeID = "node1"
- // even if seamless is being enabled by default rather than by policy, this is
- // the point where it will first get enabled.
- if seamless {
- sys.ControlKnobs().SeamlessKeyRenewal.Store(true)
- }
- cc.send(sendOpt{loginFinished: true, nm: &netmap.NetworkMap{}})
- {
- nn := notifies.drain(3)
- // Arguably it makes sense to unpause now, since the machine
- // authorization status is part of the netmap.
- //
- // BUG: backend unblocks wgengine at this point, even though
- // our machine key is not authorized. It probably should
- // wait until it gets into Starting.
- // TODO: (Currently this test doesn't detect that bug, but
- // it's visible in the logs)
- cc.assertCalls()
- c.Assert(nn[0].LoginFinished, qt.IsNotNil)
- c.Assert(nn[1].Prefs, qt.IsNotNil)
- c.Assert(nn[1].Prefs.Persist().UserProfile().LoginName, qt.Equals, "user1")
- // nn[2] is a state notification after login
- // Verify login finished but need machine auth using backend state
- c.Assert(isFullyAuthenticated(b), qt.IsTrue)
- c.Assert(needsMachineAuth(b), qt.IsTrue)
- nm := b.NetMap()
- c.Assert(nm, qt.IsNotNil)
- // For an empty netmap (after initial login), SelfNode may not be valid yet.
- // In this case, we can't check MachineAuthorized, but needsMachineAuth already verified the state.
- if nm.SelfNode.Valid() {
- c.Assert(nm.SelfNode.MachineAuthorized(), qt.IsFalse)
- }
- }
- // Pretend that the administrator has authorized our machine.
- t.Logf("\n\nMachineAuthorized")
- notifies.expect(1)
- // BUG: the real controlclient sends LoginFinished with every
- // notification while it's in StateAuthenticated, but not StateSynced.
- // It should send it exactly once, or every time we're authenticated,
- // but the current code is brittle.
- // (ie. I suspect it would be better to change false->true in send()
- // below, and do the same in the real controlclient.)
- cc.send(sendOpt{nm: &netmap.NetworkMap{
- SelfNode: (&tailcfg.Node{MachineAuthorized: true}).View(),
- }})
- {
- nn := notifies.drain(1)
- cc.assertCalls()
- // nn[0] is a state notification after machine auth granted
- c.Assert(len(nn), qt.Equals, 1)
- // Verify machine authorized using backend state
- nm := b.NetMap()
- c.Assert(nm, qt.IsNotNil)
- c.Assert(nm.SelfNode.Valid(), qt.IsTrue)
- c.Assert(nm.SelfNode.MachineAuthorized(), qt.IsTrue)
- }
- // TODO: add a fake DERP server to our fake netmap, so we can
- // transition to the Running state here.
- // TODO: test what happens when the admin forcibly deletes our key.
- // (ie. unsolicited logout)
- // TODO: test what happens when our key expires, client side.
- // (and when it gets close to expiring)
- // The user changes their preference to !WantRunning.
- t.Logf("\n\nWantRunning -> false")
- notifies.expect(2)
- b.EditPrefs(&ipn.MaskedPrefs{
- WantRunningSet: true,
- Prefs: ipn.Prefs{WantRunning: false},
- })
- {
- nn := notifies.drain(2)
- cc.assertCalls("pause")
- // BUG: I would expect Prefs to change first, and state after.
- // nn[0] is state notification, nn[1] is prefs notification
- c.Assert(nn[1].Prefs, qt.IsNotNil)
- c.Assert(nn[1].Prefs.WantRunning(), qt.IsFalse)
- }
- // The user changes their preference to WantRunning after all.
- t.Logf("\n\nWantRunning -> true")
- store.awaitWrite()
- notifies.expect(2)
- b.EditPrefs(&ipn.MaskedPrefs{
- WantRunningSet: true,
- Prefs: ipn.Prefs{WantRunning: true},
- })
- {
- nn := notifies.drain(2)
- // BUG: Login isn't needed here. We never logged out.
- cc.assertCalls("Login", "unpause")
- // BUG: I would expect Prefs to change first, and state after.
- // nn[0] is state notification, nn[1] is prefs notification
- c.Assert(nn[1].Prefs, qt.IsNotNil)
- c.Assert(nn[1].Prefs.WantRunning(), qt.IsTrue)
- c.Assert(store.sawWrite(), qt.IsTrue)
- }
- // User wants to logout.
- store.awaitWrite()
- t.Logf("\n\nLogout")
- notifies.expect(5)
- b.Logout(context.Background(), ipnauth.Self)
- {
- nn := notifies.drain(5)
- previousCC.assertCalls("pause", "Logout", "unpause", "Shutdown")
- // nn[0] is state notification (Stopped)
- c.Assert(nn[0].State, qt.IsNotNil)
- c.Assert(*nn[0].State, qt.Equals, ipn.Stopped)
- // nn[1] is prefs notification after logout
- c.Assert(nn[1].Prefs, qt.IsNotNil)
- c.Assert(nn[1].Prefs.LoggedOut(), qt.IsTrue)
- c.Assert(nn[1].Prefs.WantRunning(), qt.IsFalse)
- cc.assertCalls("New")
- // nn[2] is the initial state notification after New (NoState)
- // nn[3] is prefs notification with emptyPrefs
- c.Assert(nn[3].Prefs, qt.IsNotNil)
- c.Assert(nn[3].Prefs.LoggedOut(), qt.IsTrue)
- c.Assert(nn[3].Prefs.WantRunning(), qt.IsFalse)
- c.Assert(store.sawWrite(), qt.IsTrue)
- // nn[4] is state notification (NeedsLogin)
- // Verify logged out and needs new login using backend state
- c.Assert(needsLogin(b), qt.IsTrue)
- c.Assert(hasValidNetMap(b), qt.IsFalse)
- }
- // A second logout should be a no-op as we are in the NeedsLogin state.
- t.Logf("\n\nLogout2")
- notifies.expect(0)
- b.Logout(context.Background(), ipnauth.Self)
- {
- notifies.drain(0)
- cc.assertCalls()
- c.Assert(b.Prefs().LoggedOut(), qt.IsTrue)
- c.Assert(b.Prefs().WantRunning(), qt.IsFalse)
- // Verify still needs login
- c.Assert(needsLogin(b), qt.IsTrue)
- }
- // A third logout should also be a no-op as the cc should be in
- // AuthCantContinue state.
- t.Logf("\n\nLogout3")
- notifies.expect(3)
- b.Logout(context.Background(), ipnauth.Self)
- {
- notifies.drain(0)
- cc.assertCalls()
- c.Assert(b.Prefs().LoggedOut(), qt.IsTrue)
- c.Assert(b.Prefs().WantRunning(), qt.IsFalse)
- // Verify still needs login
- c.Assert(needsLogin(b), qt.IsTrue)
- }
- // Oh, you thought we were done? Ha! Now we have to test what
- // happens if the user exits and restarts while logged out.
- // Note that it's explicitly okay to call b.Start() over and over
- // again, every time the frontend reconnects.
- // TODO: test user switching between statekeys.
- // The frontend restarts!
- t.Logf("\n\nStart3")
- notifies.expect(2)
- c.Assert(b.Start(ipn.Options{}), qt.IsNil)
- {
- previousCC.assertShutdown(false)
- // BUG: We already called Shutdown(), no need to do it again.
- // BUG: don't unpause because we're not logged in.
- cc.assertCalls("New")
- nn := notifies.drain(2)
- cc.assertCalls()
- c.Assert(nn[0].Prefs, qt.IsNotNil)
- c.Assert(nn[0].Prefs.LoggedOut(), qt.IsTrue)
- c.Assert(nn[0].Prefs.WantRunning(), qt.IsFalse)
- // Verify notification indicates we need login
- c.Assert(nn[1].Prefs == nil || nn[1].Prefs.LoggedOut(), qt.IsTrue)
- // Verify we need login after restart
- c.Assert(needsLogin(b), qt.IsTrue)
- c.Assert(hasValidNetMap(b), qt.IsFalse)
- }
- // Explicitly set the ControlURL to avoid defaulting to [ipn.DefaultControlURL].
- // This prevents [LocalBackend] from using the production control server during tests
- // and ensures that [LocalBackend.validPopBrowserURL] returns true for the
- // fake interactive login URLs used below. Otherwise, we won't be receiving
- // BrowseToURL notifications as expected.
- // See tailscale/tailscale#11393.
- notifies.expect(1)
- b.EditPrefs(&ipn.MaskedPrefs{
- ControlURLSet: true,
- Prefs: ipn.Prefs{
- ControlURL: "https://localhost:1/",
- },
- })
- notifies.drain(1)
- t.Logf("\n\nStartLoginInteractive3")
- b.StartLoginInteractive(context.Background())
- // We've been logged out, and the previously created profile is now deleted.
- // We're attempting an interactive login for the first time with the new profile,
- // this should result in a call to the control server, which in turn should provide
- // an interactive login URL to visit.
- notifies.expect(2)
- url3 := "https://localhost:1/3"
- cc.send(sendOpt{url: url3})
- {
- nn := notifies.drain(2)
- cc.assertCalls("Login")
- c.Assert(nn[1].BrowseToURL, qt.IsNotNil)
- c.Assert(*nn[1].BrowseToURL, qt.Equals, url3)
- }
- t.Logf("%q visited", url3)
- notifies.expect(3)
- cc.persist.UserProfile.LoginName = "user2"
- cc.persist.NodeID = "node2"
- cc.send(sendOpt{loginFinished: true, nm: &netmap.NetworkMap{
- SelfNode: (&tailcfg.Node{MachineAuthorized: true}).View(),
- }})
- t.Logf("\n\nLoginFinished3")
- {
- nn := notifies.drain(3)
- c.Assert(nn[0].LoginFinished, qt.IsNotNil)
- c.Assert(nn[1].Prefs, qt.IsNotNil)
- c.Assert(nn[1].Prefs.Persist(), qt.IsNotNil)
- // Prefs after finishing the login, so LoginName updated.
- c.Assert(nn[1].Prefs.Persist().UserProfile().LoginName, qt.Equals, "user2")
- c.Assert(nn[1].Prefs.LoggedOut(), qt.IsFalse)
- // If a user initiates an interactive login, they also expect WantRunning to become true.
- c.Assert(nn[1].Prefs.WantRunning(), qt.IsTrue)
- // nn[2] is state notification (Starting) - verify using backend state
- c.Assert(isWantRunning(b), qt.IsTrue)
- c.Assert(isLoggedIn(b), qt.IsTrue)
- }
- // Now we've logged in successfully. Let's disconnect.
- t.Logf("\n\nWantRunning -> false")
- notifies.expect(2)
- b.EditPrefs(&ipn.MaskedPrefs{
- WantRunningSet: true,
- Prefs: ipn.Prefs{WantRunning: false},
- })
- {
- nn := notifies.drain(2)
- cc.assertCalls("pause")
- // BUG: I would expect Prefs to change first, and state after.
- // nn[0] is state notification (Stopped), nn[1] is prefs notification
- c.Assert(nn[1].Prefs, qt.IsNotNil)
- c.Assert(nn[1].Prefs.WantRunning(), qt.IsFalse)
- c.Assert(nn[1].Prefs.LoggedOut(), qt.IsFalse)
- }
- // One more restart, this time with a valid key, but WantRunning=false.
- t.Logf("\n\nStart4")
- notifies.expect(2)
- c.Assert(b.Start(ipn.Options{}), qt.IsNil)
- {
- // NOTE: cc.Shutdown() is correct here, since we didn't call
- // b.Shutdown() explicitly ourselves.
- previousCC.assertShutdown(false)
- nn := notifies.drain(2)
- // We already have a netmap for this node,
- // and WantRunning is false, so cc should be paused.
- cc.assertCalls("New", "Login", "pause")
- c.Assert(nn[0].Prefs, qt.IsNotNil)
- c.Assert(nn[0].Prefs.WantRunning(), qt.IsFalse)
- c.Assert(nn[0].Prefs.LoggedOut(), qt.IsFalse)
- // nn[1] is state notification (Stopped)
- // Verify backend shows we're not wanting to run
- c.Assert(isWantRunning(b), qt.IsFalse)
- }
- // When logged in but !WantRunning, ipn leaves us unpaused to retrieve
- // the first netmap. Simulate that netmap being received, after which
- // it should pause us, to avoid wasting CPU retrieving unnecessarily
- // additional netmap updates. Since our LocalBackend instance already
- // has a netmap, we will reset it to nil to simulate the first netmap
- // retrieval.
- b.setNetMapLocked(nil)
- cc.assertCalls("unpause")
- //
- // TODO: really the various GUIs and prefs should be refactored to
- // not require the netmap structure at all when starting while
- // !WantRunning. That would remove the need for this (or contacting
- // the control server at all when stopped).
- t.Logf("\n\nStart4 -> netmap")
- notifies.expect(0)
- cc.send(sendOpt{loginFinished: true, nm: &netmap.NetworkMap{
- SelfNode: (&tailcfg.Node{MachineAuthorized: true}).View(),
- }})
- {
- notifies.drain(0)
- cc.assertCalls("pause")
- }
- // Request connection.
- // The state machine didn't call Login() earlier, so now it needs to.
- t.Logf("\n\nWantRunning4 -> true")
- notifies.expect(2)
- b.EditPrefs(&ipn.MaskedPrefs{
- WantRunningSet: true,
- Prefs: ipn.Prefs{WantRunning: true},
- })
- {
- nn := notifies.drain(2)
- cc.assertCalls("Login", "unpause")
- // BUG: I would expect Prefs to change first, and state after.
- // nn[0] is state notification (Starting), nn[1] is prefs notification
- c.Assert(nn[1].Prefs, qt.IsNotNil)
- c.Assert(nn[1].Prefs.WantRunning(), qt.IsTrue)
- }
- // Disconnect.
- t.Logf("\n\nStop")
- notifies.expect(2)
- b.EditPrefs(&ipn.MaskedPrefs{
- WantRunningSet: true,
- Prefs: ipn.Prefs{WantRunning: false},
- })
- {
- nn := notifies.drain(2)
- cc.assertCalls("pause")
- // BUG: I would expect Prefs to change first, and state after.
- // nn[0] is state notification (Stopped), nn[1] is prefs notification
- c.Assert(nn[1].Prefs, qt.IsNotNil)
- c.Assert(nn[1].Prefs.WantRunning(), qt.IsFalse)
- }
- // We want to try logging in as a different user, while Stopped.
- // First, start the login process (without logging out first).
- t.Logf("\n\nLoginDifferent")
- notifies.expect(1)
- b.StartLoginInteractive(context.Background())
- url4 := "https://localhost:1/4"
- cc.send(sendOpt{url: url4})
- {
- nn := notifies.drain(1)
- // It might seem like WantRunning should switch to true here,
- // but that would be risky since we already have a valid
- // user account. It might try to reconnect to the old account
- // before the new one is ready. So no change yet.
- //
- // Because the login hasn't yet completed, the old login
- // is still valid, so it's correct that we stay paused.
- cc.assertCalls("Login")
- c.Assert(nn[0].BrowseToURL, qt.IsNotNil)
- c.Assert(*nn[0].BrowseToURL, qt.Equals, url4)
- }
- // Now, let's complete the interactive login, using a different
- // user account than before. WantRunning changes to true after an
- // interactive login, so we end up unpaused.
- t.Logf("\n\nLoginDifferent URL visited")
- notifies.expect(3)
- cc.persist.UserProfile.LoginName = "user3"
- cc.persist.NodeID = "node3"
- cc.send(sendOpt{loginFinished: true, nm: &netmap.NetworkMap{
- SelfNode: (&tailcfg.Node{MachineAuthorized: true}).View(),
- }})
- {
- nn := notifies.drain(3)
- // BUG: pause() being called here is a bad sign.
- // It means that either the state machine ran at least once
- // with the old netmap, or it ran with the new login+netmap
- // and !WantRunning. But since it's a fresh and successful
- // new login, WantRunning is true, so there was never a
- // reason to pause().
- cc.assertCalls("unpause")
- c.Assert(nn[0].LoginFinished, qt.IsNotNil)
- c.Assert(nn[1].Prefs, qt.IsNotNil)
- // Prefs after finishing the login, so LoginName updated.
- c.Assert(nn[1].Prefs.Persist().UserProfile().LoginName, qt.Equals, "user3")
- c.Assert(nn[1].Prefs.LoggedOut(), qt.IsFalse)
- c.Assert(nn[1].Prefs.WantRunning(), qt.IsTrue)
- // nn[2] is state notification (Starting) - verify using backend state
- c.Assert(isWantRunning(b), qt.IsTrue)
- c.Assert(isLoggedIn(b), qt.IsTrue)
- }
- // The last test case is the most common one: restarting when both
- // logged in and WantRunning.
- t.Logf("\n\nStart5")
- notifies.expect(2)
- c.Assert(b.Start(ipn.Options{}), qt.IsNil)
- {
- // NOTE: cc.Shutdown() is correct here, since we didn't call
- // b.Shutdown() ourselves.
- previousCC.assertShutdown(false)
- cc.assertCalls("New", "Login")
- nn := notifies.drain(2)
- cc.assertCalls()
- c.Assert(nn[0].Prefs, qt.IsNotNil)
- c.Assert(nn[0].Prefs.LoggedOut(), qt.IsFalse)
- c.Assert(nn[0].Prefs.WantRunning(), qt.IsTrue)
- // nn[1] is state notification (Starting)
- // Verify we're authenticated with valid netmap using backend state
- c.Assert(isFullyAuthenticated(b), qt.IsTrue)
- c.Assert(hasValidNetMap(b), qt.IsTrue)
- }
- // Control server accepts our valid key from before.
- t.Logf("\n\nLoginFinished5")
- notifies.expect(0)
- cc.send(sendOpt{loginFinished: true, nm: &netmap.NetworkMap{
- SelfNode: (&tailcfg.Node{MachineAuthorized: true}).View(),
- }})
- {
- notifies.drain(0)
- cc.assertCalls()
- // NOTE: No LoginFinished message since no interactive
- // login was needed.
- // NOTE: No prefs change this time. WantRunning stays true.
- // We were in Starting in the first place, so that doesn't
- // change either, so we don't expect any notifications.
- // Verify we're still authenticated with valid netmap
- c.Assert(isFullyAuthenticated(b), qt.IsTrue)
- c.Assert(hasValidNetMap(b), qt.IsTrue)
- }
- t.Logf("\n\nExpireKey")
- notifies.expect(1)
- cc.send(sendOpt{nm: &netmap.NetworkMap{
- SelfNode: (&tailcfg.Node{
- KeyExpiry: time.Now().Add(-time.Minute),
- MachineAuthorized: true,
- }).View(),
- }})
- {
- nn := notifies.drain(1)
- cc.assertCalls()
- // nn[0] is state notification (NeedsLogin) due to key expiry
- c.Assert(len(nn), qt.Equals, 1)
- // Verify key expired, need new login using backend state
- c.Assert(needsLogin(b), qt.IsTrue)
- c.Assert(b.isEngineBlocked(), qt.IsTrue)
- }
- t.Logf("\n\nExtendKey")
- notifies.expect(1)
- cc.send(sendOpt{nm: &netmap.NetworkMap{
- SelfNode: (&tailcfg.Node{
- MachineAuthorized: true,
- KeyExpiry: time.Now().Add(time.Minute),
- }).View(),
- }})
- {
- nn := notifies.drain(1)
- cc.assertCalls()
- // nn[0] is state notification (Starting) after key extension
- c.Assert(len(nn), qt.Equals, 1)
- // Verify key extended, authenticated again using backend state
- c.Assert(isFullyAuthenticated(b), qt.IsTrue)
- c.Assert(hasValidNetMap(b), qt.IsTrue)
- c.Assert(b.isEngineBlocked(), qt.IsFalse)
- }
- notifies.expect(1)
- // Fake a DERP connection.
- b.setWgengineStatus(&wgengine.Status{DERPs: 1, AsOf: time.Now()}, nil)
- {
- nn := notifies.drain(1)
- cc.assertCalls()
- // nn[0] is state notification (Running) after DERP connection
- c.Assert(len(nn), qt.Equals, 1)
- // Verify we can route traffic using backend state
- c.Assert(canRouteTraffic(b), qt.IsTrue)
- }
- }
- func TestEditPrefsHasNoKeys(t *testing.T) {
- logf := tstest.WhileTestRunningLogger(t)
- sys := tsd.NewSystem()
- sys.Set(new(mem.Store))
- e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker.Get(), sys.UserMetricsRegistry(), sys.Bus.Get())
- if err != nil {
- t.Fatalf("NewFakeUserspaceEngine: %v", err)
- }
- t.Cleanup(e.Close)
- sys.Set(e)
- b, err := NewLocalBackend(logf, logid.PublicID{}, sys, 0)
- if err != nil {
- t.Fatalf("NewLocalBackend: %v", err)
- }
- t.Cleanup(b.Shutdown)
- b.hostinfo = &tailcfg.Hostinfo{OS: "testos"}
- b.pm.SetPrefs((&ipn.Prefs{
- Persist: &persist.Persist{
- PrivateNodeKey: key.NewNode(),
- OldPrivateNodeKey: key.NewNode(),
- },
- }).View(), ipn.NetworkProfile{})
- if p := b.pm.CurrentPrefs().Persist(); !p.Valid() || p.PrivateNodeKey().IsZero() {
- t.Fatalf("PrivateNodeKey not set")
- }
- p, err := b.EditPrefs(&ipn.MaskedPrefs{
- Prefs: ipn.Prefs{
- Hostname: "foo",
- },
- HostnameSet: true,
- })
- if err != nil {
- t.Fatalf("EditPrefs: %v", err)
- }
- if p.Hostname() != "foo" {
- t.Errorf("Hostname = %q; want foo", p.Hostname())
- }
- if !p.Persist().PrivateNodeKey().IsZero() {
- t.Errorf("PrivateNodeKey = %v; want zero", p.Persist().PrivateNodeKey())
- }
- if !p.Persist().OldPrivateNodeKey().IsZero() {
- t.Errorf("OldPrivateNodeKey = %v; want zero", p.Persist().OldPrivateNodeKey())
- }
- if !p.Persist().NetworkLockKey().IsZero() {
- t.Errorf("NetworkLockKey= %v; want zero", p.Persist().NetworkLockKey())
- }
- }
- type testStateStorage struct {
- mem.Store
- written atomic.Bool
- }
- func (s *testStateStorage) WriteState(id ipn.StateKey, bs []byte) error {
- s.written.Store(true)
- return s.Store.WriteState(id, bs)
- }
- // awaitWrite clears the "I've seen writes" bit, in prep for a future
- // call to sawWrite to see if a write arrived.
- func (s *testStateStorage) awaitWrite() { s.written.Store(false) }
- // sawWrite reports whether there's been a WriteState call since the most
- // recent awaitWrite call.
- func (s *testStateStorage) sawWrite() bool {
- v := s.written.Load()
- s.awaitWrite()
- return v
- }
- func TestWGEngineStatusRace(t *testing.T) {
- t.Skip("test fails")
- c := qt.New(t)
- logf := tstest.WhileTestRunningLogger(t)
- sys := tsd.NewSystem()
- sys.Set(new(mem.Store))
- eng, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.Bus.Get())
- c.Assert(err, qt.IsNil)
- t.Cleanup(eng.Close)
- sys.Set(eng)
- b, err := NewLocalBackend(logf, logid.PublicID{}, sys, 0)
- c.Assert(err, qt.IsNil)
- t.Cleanup(b.Shutdown)
- var cc *mockControl
- b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) {
- cc = newClient(t, opts)
- return cc, nil
- })
- var state ipn.State
- b.SetNotifyCallback(func(n ipn.Notify) {
- if n.State != nil {
- state = *n.State
- }
- })
- wantState := func(want ipn.State) {
- c.Assert(want, qt.Equals, state)
- }
- // Start with the zero value.
- wantState(ipn.NoState)
- // Start the backend.
- err = b.Start(ipn.Options{})
- c.Assert(err, qt.IsNil)
- wantState(ipn.NeedsLogin)
- // Assert that we are logged in and authorized.
- cc.send(sendOpt{loginFinished: true, nm: &netmap.NetworkMap{
- SelfNode: (&tailcfg.Node{MachineAuthorized: true}).View(),
- }})
- wantState(ipn.Starting)
- // Simulate multiple concurrent callbacks from wgengine.
- // Any single callback with DERPS > 0 is enough to transition
- // from Starting to Running, at which point we stay there.
- // Thus if these callbacks occurred serially, in any order,
- // we would end up in state ipn.Running.
- // The same should thus be true if these callbacks occur concurrently.
- var wg sync.WaitGroup
- for i := range 100 {
- wg.Add(1)
- go func(i int) {
- defer wg.Done()
- n := 0
- if i == 0 {
- n = 1
- }
- b.setWgengineStatus(&wgengine.Status{AsOf: time.Now(), DERPs: n}, nil)
- }(i)
- }
- wg.Wait()
- wantState(ipn.Running)
- }
- // TestEngineReconfigOnStateChange verifies that wgengine is properly reconfigured
- // when the LocalBackend's state changes, such as when the user logs in, switches
- // profiles, or disconnects from Tailscale.
- func TestEngineReconfigOnStateChange(t *testing.T) {
- enableLogging := false
- connect := &ipn.MaskedPrefs{Prefs: ipn.Prefs{WantRunning: true}, WantRunningSet: true}
- disconnect := &ipn.MaskedPrefs{Prefs: ipn.Prefs{WantRunning: false}, WantRunningSet: true}
- node1 := buildNetmapWithPeers(
- makePeer(1, withName("node-1"), withAddresses(netip.MustParsePrefix("100.64.1.1/32"))),
- )
- node2 := buildNetmapWithPeers(
- makePeer(2, withName("node-2"), withAddresses(netip.MustParsePrefix("100.64.1.2/32"))),
- )
- node3 := buildNetmapWithPeers(
- makePeer(3, withName("node-3"), withAddresses(netip.MustParsePrefix("100.64.1.3/32"))),
- node1.SelfNode,
- node2.SelfNode,
- )
- routesWithQuad100 := func(extra ...netip.Prefix) []netip.Prefix {
- return append(extra, netip.MustParsePrefix("100.100.100.100/32"))
- }
- hostsFor := func(nm *netmap.NetworkMap) map[dnsname.FQDN][]netip.Addr {
- var hosts map[dnsname.FQDN][]netip.Addr
- appendNode := func(n tailcfg.NodeView) {
- addrs := make([]netip.Addr, 0, n.Addresses().Len())
- for _, addr := range n.Addresses().All() {
- addrs = append(addrs, addr.Addr())
- }
- mak.Set(&hosts, must.Get(dnsname.ToFQDN(n.Name())), addrs)
- }
- if nm != nil && nm.SelfNode.Valid() {
- appendNode(nm.SelfNode)
- }
- for _, n := range nm.Peers {
- appendNode(n)
- }
- return hosts
- }
- tests := []struct {
- name string
- steps func(*testing.T, *LocalBackend, func() *mockControl)
- wantState ipn.State
- wantCfg *wgcfg.Config
- wantRouterCfg *router.Config
- wantDNSCfg *dns.Config
- }{
- {
- name: "Initial",
- // The configs are nil until the the LocalBackend is started.
- wantState: ipn.NoState,
- wantCfg: nil,
- wantRouterCfg: nil,
- wantDNSCfg: nil,
- },
- {
- name: "Start",
- steps: func(t *testing.T, lb *LocalBackend, _ func() *mockControl) {
- mustDo(t)(lb.Start(ipn.Options{}))
- },
- // Once started, all configs must be reset and have their zero values.
- wantState: ipn.NeedsLogin,
- wantCfg: &wgcfg.Config{},
- wantRouterCfg: &router.Config{},
- wantDNSCfg: &dns.Config{},
- },
- {
- name: "Start/Connect",
- steps: func(t *testing.T, lb *LocalBackend, _ func() *mockControl) {
- mustDo(t)(lb.Start(ipn.Options{}))
- mustDo2(t)(lb.EditPrefs(connect))
- },
- // Same if WantRunning is true, but the auth is not completed yet.
- wantState: ipn.NeedsLogin,
- wantCfg: &wgcfg.Config{},
- wantRouterCfg: &router.Config{},
- wantDNSCfg: &dns.Config{},
- },
- {
- name: "Start/Connect/Login",
- steps: func(t *testing.T, lb *LocalBackend, cc func() *mockControl) {
- mustDo(t)(lb.Start(ipn.Options{}))
- mustDo2(t)(lb.EditPrefs(connect))
- cc().authenticated(node1)
- },
- // After the auth is completed, the configs must be updated to reflect the node's netmap.
- wantState: ipn.Starting,
- wantCfg: &wgcfg.Config{
- Peers: []wgcfg.Peer{},
- Addresses: node1.SelfNode.Addresses().AsSlice(),
- },
- wantRouterCfg: &router.Config{
- SNATSubnetRoutes: true,
- NetfilterMode: preftype.NetfilterOn,
- LocalAddrs: node1.SelfNode.Addresses().AsSlice(),
- Routes: routesWithQuad100(),
- },
- wantDNSCfg: &dns.Config{
- Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
- Hosts: hostsFor(node1),
- },
- },
- {
- name: "Start/Connect/Login/Disconnect",
- steps: func(t *testing.T, lb *LocalBackend, cc func() *mockControl) {
- mustDo(t)(lb.Start(ipn.Options{}))
- mustDo2(t)(lb.EditPrefs(connect))
- cc().authenticated(node1)
- mustDo2(t)(lb.EditPrefs(disconnect))
- },
- // After disconnecting, all configs must be reset and have their zero values.
- wantState: ipn.Stopped,
- wantCfg: &wgcfg.Config{},
- wantRouterCfg: &router.Config{},
- wantDNSCfg: &dns.Config{},
- },
- {
- name: "Start/Connect/Login/NewProfile",
- steps: func(t *testing.T, lb *LocalBackend, cc func() *mockControl) {
- mustDo(t)(lb.Start(ipn.Options{}))
- mustDo2(t)(lb.EditPrefs(connect))
- cc().authenticated(node1)
- mustDo(t)(lb.NewProfile())
- },
- // After switching to a new, empty profile, all configs should be reset
- // and have their zero values until the auth is completed.
- wantState: ipn.NeedsLogin,
- wantCfg: &wgcfg.Config{},
- wantRouterCfg: &router.Config{},
- wantDNSCfg: &dns.Config{},
- },
- {
- name: "Start/Connect/Login/NewProfile/Login",
- steps: func(t *testing.T, lb *LocalBackend, cc func() *mockControl) {
- mustDo(t)(lb.Start(ipn.Options{}))
- mustDo2(t)(lb.EditPrefs(connect))
- cc().authenticated(node1)
- mustDo(t)(lb.NewProfile())
- mustDo2(t)(lb.EditPrefs(connect))
- cc().authenticated(node2)
- },
- // Once the auth is completed, the configs must be updated to reflect the node's netmap.
- wantState: ipn.Starting,
- wantCfg: &wgcfg.Config{
- Peers: []wgcfg.Peer{},
- Addresses: node2.SelfNode.Addresses().AsSlice(),
- },
- wantRouterCfg: &router.Config{
- SNATSubnetRoutes: true,
- NetfilterMode: preftype.NetfilterOn,
- LocalAddrs: node2.SelfNode.Addresses().AsSlice(),
- Routes: routesWithQuad100(),
- },
- wantDNSCfg: &dns.Config{
- Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
- Hosts: hostsFor(node2),
- },
- },
- {
- name: "Start/Connect/Login/SwitchProfile",
- steps: func(t *testing.T, lb *LocalBackend, cc func() *mockControl) {
- mustDo(t)(lb.Start(ipn.Options{}))
- mustDo2(t)(lb.EditPrefs(connect))
- cc().authenticated(node1)
- profileID := lb.CurrentProfile().ID()
- mustDo(t)(lb.NewProfile())
- cc().authenticated(node2)
- mustDo(t)(lb.SwitchProfile(profileID))
- },
- // After switching to an existing profile, all configs must be reset
- // and have their zero values until the (non-interactive) login is completed.
- wantState: ipn.NoState,
- wantCfg: &wgcfg.Config{},
- wantRouterCfg: &router.Config{},
- wantDNSCfg: &dns.Config{},
- },
- {
- name: "Start/Connect/Login/SwitchProfile/NonInteractiveLogin",
- steps: func(t *testing.T, lb *LocalBackend, cc func() *mockControl) {
- mustDo(t)(lb.Start(ipn.Options{}))
- mustDo2(t)(lb.EditPrefs(connect))
- cc().authenticated(node1)
- profileID := lb.CurrentProfile().ID()
- mustDo(t)(lb.NewProfile())
- cc().authenticated(node2)
- mustDo(t)(lb.SwitchProfile(profileID))
- cc().authenticated(node1) // complete the login
- },
- // After switching profiles and completing the auth, the configs
- // must be updated to reflect the node's netmap.
- wantState: ipn.Starting,
- wantCfg: &wgcfg.Config{
- Peers: []wgcfg.Peer{},
- Addresses: node1.SelfNode.Addresses().AsSlice(),
- },
- wantRouterCfg: &router.Config{
- SNATSubnetRoutes: true,
- NetfilterMode: preftype.NetfilterOn,
- LocalAddrs: node1.SelfNode.Addresses().AsSlice(),
- Routes: routesWithQuad100(),
- },
- wantDNSCfg: &dns.Config{
- Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
- Hosts: hostsFor(node1),
- },
- },
- {
- name: "Start/Connect/Login/WithPeers",
- steps: func(t *testing.T, lb *LocalBackend, cc func() *mockControl) {
- mustDo(t)(lb.Start(ipn.Options{}))
- mustDo2(t)(lb.EditPrefs(connect))
- cc().authenticated(node3)
- },
- wantState: ipn.Starting,
- wantCfg: &wgcfg.Config{
- Peers: []wgcfg.Peer{
- {
- PublicKey: node1.SelfNode.Key(),
- DiscoKey: node1.SelfNode.DiscoKey(),
- },
- {
- PublicKey: node2.SelfNode.Key(),
- DiscoKey: node2.SelfNode.DiscoKey(),
- },
- },
- Addresses: node3.SelfNode.Addresses().AsSlice(),
- },
- wantRouterCfg: &router.Config{
- SNATSubnetRoutes: true,
- NetfilterMode: preftype.NetfilterOn,
- LocalAddrs: node3.SelfNode.Addresses().AsSlice(),
- Routes: routesWithQuad100(),
- },
- wantDNSCfg: &dns.Config{
- Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
- Hosts: hostsFor(node3),
- },
- },
- {
- name: "Start/Connect/Login/Expire",
- steps: func(t *testing.T, lb *LocalBackend, cc func() *mockControl) {
- mustDo(t)(lb.Start(ipn.Options{}))
- mustDo2(t)(lb.EditPrefs(connect))
- cc().authenticated(node1)
- cc().send(sendOpt{nm: &netmap.NetworkMap{
- SelfNode: (&tailcfg.Node{
- KeyExpiry: time.Now().Add(-time.Minute),
- }).View(),
- }})
- },
- wantState: ipn.NeedsLogin,
- wantCfg: &wgcfg.Config{},
- wantRouterCfg: &router.Config{},
- wantDNSCfg: &dns.Config{},
- },
- {
- name: "Start/Connect/Login/InitReauth",
- steps: func(t *testing.T, lb *LocalBackend, cc func() *mockControl) {
- mustDo(t)(lb.Start(ipn.Options{}))
- mustDo2(t)(lb.EditPrefs(connect))
- cc().authenticated(node1)
- // Start the re-auth process:
- lb.StartLoginInteractive(context.Background())
- cc().sendAuthURL(node1)
- },
- // Without seamless renewal, even starting a reauth tears down everything:
- wantState: ipn.Starting,
- wantCfg: &wgcfg.Config{},
- wantRouterCfg: &router.Config{},
- wantDNSCfg: &dns.Config{},
- },
- {
- name: "Start/Connect/Login/InitReauth/Login",
- steps: func(t *testing.T, lb *LocalBackend, cc func() *mockControl) {
- mustDo(t)(lb.Start(ipn.Options{}))
- mustDo2(t)(lb.EditPrefs(connect))
- cc().authenticated(node1)
- // Start the re-auth process:
- lb.StartLoginInteractive(context.Background())
- cc().sendAuthURL(node1)
- // Complete the re-auth process:
- cc().authenticated(node1)
- },
- wantState: ipn.Starting,
- wantCfg: &wgcfg.Config{
- Peers: []wgcfg.Peer{},
- Addresses: node1.SelfNode.Addresses().AsSlice(),
- },
- wantRouterCfg: &router.Config{
- SNATSubnetRoutes: true,
- NetfilterMode: preftype.NetfilterOn,
- LocalAddrs: node1.SelfNode.Addresses().AsSlice(),
- Routes: routesWithQuad100(),
- },
- wantDNSCfg: &dns.Config{
- Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
- Hosts: hostsFor(node1),
- },
- },
- {
- name: "Seamless/Start/Connect/Login/InitReauth",
- steps: func(t *testing.T, lb *LocalBackend, cc func() *mockControl) {
- lb.ControlKnobs().SeamlessKeyRenewal.Store(true)
- mustDo(t)(lb.Start(ipn.Options{}))
- mustDo2(t)(lb.EditPrefs(connect))
- cc().authenticated(node1)
- // Start the re-auth process:
- lb.StartLoginInteractive(context.Background())
- cc().sendAuthURL(node1)
- },
- // With seamless renewal, starting a reauth should leave everything up:
- wantState: ipn.Starting,
- wantCfg: &wgcfg.Config{
- Peers: []wgcfg.Peer{},
- Addresses: node1.SelfNode.Addresses().AsSlice(),
- },
- wantRouterCfg: &router.Config{
- SNATSubnetRoutes: true,
- NetfilterMode: preftype.NetfilterOn,
- LocalAddrs: node1.SelfNode.Addresses().AsSlice(),
- Routes: routesWithQuad100(),
- },
- wantDNSCfg: &dns.Config{
- Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
- Hosts: hostsFor(node1),
- },
- },
- {
- name: "Seamless/Start/Connect/Login/InitReauth/Login",
- steps: func(t *testing.T, lb *LocalBackend, cc func() *mockControl) {
- lb.ControlKnobs().SeamlessKeyRenewal.Store(true)
- mustDo(t)(lb.Start(ipn.Options{}))
- mustDo2(t)(lb.EditPrefs(connect))
- cc().authenticated(node1)
- // Start the re-auth process:
- lb.StartLoginInteractive(context.Background())
- cc().sendAuthURL(node1)
- // Complete the re-auth process:
- cc().authenticated(node1)
- },
- wantState: ipn.Starting,
- wantCfg: &wgcfg.Config{
- Peers: []wgcfg.Peer{},
- Addresses: node1.SelfNode.Addresses().AsSlice(),
- },
- wantRouterCfg: &router.Config{
- SNATSubnetRoutes: true,
- NetfilterMode: preftype.NetfilterOn,
- LocalAddrs: node1.SelfNode.Addresses().AsSlice(),
- Routes: routesWithQuad100(),
- },
- wantDNSCfg: &dns.Config{
- Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
- Hosts: hostsFor(node1),
- },
- },
- {
- name: "Seamless/Start/Connect/Login/Expire",
- steps: func(t *testing.T, lb *LocalBackend, cc func() *mockControl) {
- lb.ControlKnobs().SeamlessKeyRenewal.Store(true)
- mustDo(t)(lb.Start(ipn.Options{}))
- mustDo2(t)(lb.EditPrefs(connect))
- cc().authenticated(node1)
- cc().send(sendOpt{nm: &netmap.NetworkMap{
- SelfNode: (&tailcfg.Node{
- KeyExpiry: time.Now().Add(-time.Minute),
- }).View(),
- }})
- },
- // Even with seamless, if the key we are using expires, we want to disconnect:
- wantState: ipn.NeedsLogin,
- wantCfg: &wgcfg.Config{},
- wantRouterCfg: &router.Config{},
- wantDNSCfg: &dns.Config{},
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- lb, engine, cc := newLocalBackendWithMockEngineAndControl(t, enableLogging)
- if tt.steps != nil {
- tt.steps(t, lb, cc)
- }
- // TODO(bradfitz): this whole event bus settling thing
- // should be unnecessary once the bogus uses of eventbus
- // are removed. (https://github.com/tailscale/tailscale/issues/16369)
- lb.settleEventBus()
- if gotState := lb.State(); gotState != tt.wantState {
- t.Errorf("State: got %v; want %v", gotState, tt.wantState)
- }
- if engine.Config() != nil {
- for _, p := range engine.Config().Peers {
- pKey := p.PublicKey.UntypedHexString()
- _, err := lb.MagicConn().ParseEndpoint(pKey)
- if err != nil {
- t.Errorf("ParseEndpoint(%q) failed: %v", pKey, err)
- }
- }
- }
- opts := []cmp.Option{
- cmpopts.EquateComparable(key.NodePublic{}, key.DiscoPublic{}, netip.Addr{}, netip.Prefix{}),
- }
- if diff := cmp.Diff(tt.wantCfg, engine.Config(), opts...); diff != "" {
- t.Errorf("wgcfg.Config(+got -want): %v", diff)
- }
- if diff := cmp.Diff(tt.wantRouterCfg, engine.RouterConfig(), opts...); diff != "" {
- t.Errorf("router.Config(+got -want): %v", diff)
- }
- if diff := cmp.Diff(tt.wantDNSCfg, engine.DNSConfig(), opts...); diff != "" {
- t.Errorf("dns.Config(+got -want): %v", diff)
- }
- })
- }
- }
- // TestSendPreservesAuthURL tests that wgengine updates arriving in the middle of
- // processing an auth URL doesn't result in the auth URL being cleared.
- func TestSendPreservesAuthURL(t *testing.T) {
- runTestSendPreservesAuthURL(t, false)
- }
- func TestSendPreservesAuthURLSeamless(t *testing.T) {
- runTestSendPreservesAuthURL(t, true)
- }
- func runTestSendPreservesAuthURL(t *testing.T, seamless bool) {
- var cc *mockControl
- b := newLocalBackendWithTestControl(t, true, func(tb testing.TB, opts controlclient.Options) controlclient.Client {
- cc = newClient(t, opts)
- return cc
- })
- t.Logf("Start")
- b.Start(ipn.Options{
- UpdatePrefs: &ipn.Prefs{
- WantRunning: true,
- ControlURL: "https://localhost:1/",
- },
- })
- t.Logf("LoginFinished")
- cc.persist.UserProfile.LoginName = "user1"
- cc.persist.NodeID = "node1"
- if seamless {
- b.sys.ControlKnobs().SeamlessKeyRenewal.Store(true)
- }
- cc.send(sendOpt{loginFinished: true, nm: &netmap.NetworkMap{
- SelfNode: (&tailcfg.Node{MachineAuthorized: true}).View(),
- }})
- t.Logf("Running")
- b.setWgengineStatus(&wgengine.Status{AsOf: time.Now(), DERPs: 1}, nil)
- t.Logf("Re-auth (StartLoginInteractive)")
- b.StartLoginInteractive(t.Context())
- t.Logf("Re-auth (receive URL)")
- url1 := "https://localhost:1/1"
- cc.send(sendOpt{url: url1})
- // Don't need to wait on anything else - once .send completes, authURL should
- // be set, and once .send has completed, any opportunities for a WG engine
- // status update to trample it have ended as well.
- if b.authURL == "" {
- t.Fatalf("expected authURL to be set")
- } else {
- t.Log("authURL was set")
- }
- }
- func buildNetmapWithPeers(self tailcfg.NodeView, peers ...tailcfg.NodeView) *netmap.NetworkMap {
- const (
- firstAutoUserID = tailcfg.UserID(10000)
- domain = "example.com"
- magicDNSSuffix = ".test.ts.net"
- )
- users := make(map[tailcfg.UserID]tailcfg.UserProfileView)
- makeUserForNode := func(n *tailcfg.Node) {
- var user *tailcfg.UserProfile
- if n.User == 0 {
- n.User = firstAutoUserID + tailcfg.UserID(n.ID)
- user = &tailcfg.UserProfile{
- DisplayName: n.Name,
- LoginName: n.Name,
- }
- } else if _, ok := users[n.User]; !ok {
- user = &tailcfg.UserProfile{
- DisplayName: fmt.Sprintf("User %d", n.User),
- LoginName: fmt.Sprintf("user-%d", n.User),
- }
- }
- if user != nil {
- user.ID = n.User
- user.LoginName = strings.Join([]string{user.LoginName, domain}, "@")
- users[n.User] = user.View()
- }
- }
- derpmap := &tailcfg.DERPMap{
- Regions: make(map[int]*tailcfg.DERPRegion),
- }
- makeDERPRegionForNode := func(n *tailcfg.Node) {
- if n.HomeDERP == 0 {
- return // no DERP region
- }
- if _, ok := derpmap.Regions[n.HomeDERP]; !ok {
- r := &tailcfg.DERPRegion{
- RegionID: n.HomeDERP,
- RegionName: fmt.Sprintf("Region %d", n.HomeDERP),
- }
- r.Nodes = append(r.Nodes, &tailcfg.DERPNode{
- Name: fmt.Sprintf("%da", n.HomeDERP),
- RegionID: n.HomeDERP,
- })
- derpmap.Regions[n.HomeDERP] = r
- }
- }
- updateNode := func(n tailcfg.NodeView) tailcfg.NodeView {
- mut := n.AsStruct()
- makeUserForNode(mut)
- makeDERPRegionForNode(mut)
- mut.Name = mut.Name + magicDNSSuffix
- return mut.View()
- }
- self = updateNode(self)
- for i := range peers {
- peers[i] = updateNode(peers[i])
- }
- return &netmap.NetworkMap{
- SelfNode: self,
- Domain: domain,
- Peers: peers,
- UserProfiles: users,
- DERPMap: derpmap,
- }
- }
- func mustDo(t *testing.T) func(error) {
- t.Helper()
- return func(err error) {
- t.Helper()
- if err != nil {
- t.Fatal(err)
- }
- }
- }
- func mustDo2(t *testing.T) func(any, error) {
- t.Helper()
- return func(_ any, err error) {
- t.Helper()
- if err != nil {
- t.Fatal(err)
- }
- }
- }
- func newLocalBackendWithMockEngineAndControl(t *testing.T, enableLogging bool) (*LocalBackend, *mockEngine, func() *mockControl) {
- t.Helper()
- logf := logger.Discard
- if enableLogging {
- logf = tstest.WhileTestRunningLogger(t)
- }
- dialer := &tsdial.Dialer{Logf: logf}
- dialer.SetNetMon(netmon.NewStatic())
- bus := eventbustest.NewBus(t)
- sys := tsd.NewSystemWithBus(bus)
- sys.Set(dialer)
- sys.Set(dialer.NetMon())
- dialer.SetBus(bus)
- magicConn, err := magicsock.NewConn(magicsock.Options{
- Logf: logf,
- EventBus: sys.Bus.Get(),
- NetMon: dialer.NetMon(),
- Metrics: sys.UserMetricsRegistry(),
- HealthTracker: sys.HealthTracker.Get(),
- DisablePortMapper: true,
- })
- if err != nil {
- t.Fatalf("NewConn failed: %v", err)
- }
- magicConn.SetNetworkUp(dialer.NetMon().InterfaceState().AnyInterfaceUp())
- sys.Set(magicConn)
- engine := newMockEngine()
- sys.Set(engine)
- t.Cleanup(func() {
- engine.Close()
- <-engine.Done()
- })
- lb := newLocalBackendWithSysAndTestControl(t, enableLogging, sys, func(tb testing.TB, opts controlclient.Options) controlclient.Client {
- return newClient(tb, opts)
- })
- return lb, engine, func() *mockControl { return lb.cc.(*mockControl) }
- }
- var _ wgengine.Engine = (*mockEngine)(nil)
- // mockEngine implements [wgengine.Engine].
- type mockEngine struct {
- done chan struct{} // closed when Close is called
- mu sync.Mutex // protects all following fields
- closed bool
- cfg *wgcfg.Config
- routerCfg *router.Config
- dnsCfg *dns.Config
- filter, jailedFilter *filter.Filter
- statusCb wgengine.StatusCallback
- }
- func newMockEngine() *mockEngine {
- return &mockEngine{
- done: make(chan struct{}),
- }
- }
- func (e *mockEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config, dnsCfg *dns.Config) error {
- e.mu.Lock()
- defer e.mu.Unlock()
- if e.closed {
- return errors.New("engine closed")
- }
- e.cfg = cfg
- e.routerCfg = routerCfg
- e.dnsCfg = dnsCfg
- return nil
- }
- func (e *mockEngine) Config() *wgcfg.Config {
- e.mu.Lock()
- defer e.mu.Unlock()
- return e.cfg
- }
- func (e *mockEngine) RouterConfig() *router.Config {
- e.mu.Lock()
- defer e.mu.Unlock()
- return e.routerCfg
- }
- func (e *mockEngine) DNSConfig() *dns.Config {
- e.mu.Lock()
- defer e.mu.Unlock()
- return e.dnsCfg
- }
- func (e *mockEngine) PeerForIP(netip.Addr) (_ wgengine.PeerForIP, ok bool) {
- return wgengine.PeerForIP{}, false
- }
- func (e *mockEngine) GetFilter() *filter.Filter {
- e.mu.Lock()
- defer e.mu.Unlock()
- return e.filter
- }
- func (e *mockEngine) SetFilter(f *filter.Filter) {
- e.mu.Lock()
- e.filter = f
- e.mu.Unlock()
- }
- func (e *mockEngine) GetJailedFilter() *filter.Filter {
- e.mu.Lock()
- defer e.mu.Unlock()
- return e.jailedFilter
- }
- func (e *mockEngine) SetJailedFilter(f *filter.Filter) {
- e.mu.Lock()
- e.jailedFilter = f
- e.mu.Unlock()
- }
- func (e *mockEngine) SetStatusCallback(cb wgengine.StatusCallback) {
- e.mu.Lock()
- e.statusCb = cb
- e.mu.Unlock()
- }
- func (e *mockEngine) RequestStatus() {
- e.mu.Lock()
- cb := e.statusCb
- e.mu.Unlock()
- if cb != nil {
- cb(&wgengine.Status{AsOf: time.Now()}, nil)
- }
- }
- func (e *mockEngine) ResetAndStop() (*wgengine.Status, error) {
- err := e.Reconfig(&wgcfg.Config{}, &router.Config{}, &dns.Config{})
- if err != nil {
- return nil, err
- }
- return &wgengine.Status{AsOf: time.Now()}, nil
- }
- func (e *mockEngine) PeerByKey(key.NodePublic) (_ wgint.Peer, ok bool) {
- return wgint.Peer{}, false
- }
- func (e *mockEngine) SetNetworkMap(*netmap.NetworkMap) {}
- func (e *mockEngine) UpdateStatus(*ipnstate.StatusBuilder) {}
- func (e *mockEngine) Ping(ip netip.Addr, pingType tailcfg.PingType, size int, cb func(*ipnstate.PingResult)) {
- cb(&ipnstate.PingResult{IP: ip.String(), Err: "not implemented"})
- }
- func (e *mockEngine) InstallCaptureHook(packet.CaptureCallback) {}
- func (e *mockEngine) Close() {
- e.mu.Lock()
- defer e.mu.Unlock()
- if e.closed {
- return
- }
- e.closed = true
- close(e.done)
- }
- func (e *mockEngine) Done() <-chan struct{} {
- return e.done
- }
- // hasValidNetMap returns true if the backend has a valid network map with a valid self node.
- func hasValidNetMap(b *LocalBackend) bool {
- nm := b.NetMap()
- return nm != nil && nm.SelfNode.Valid()
- }
- // needsLogin returns true if the backend needs user login action.
- // This is true when logged out, when an auth URL is present (interactive login in progress),
- // or when the node key has expired.
- func needsLogin(b *LocalBackend) bool {
- // Note: b.Prefs() handles its own locking, so we lock only for authURL and keyExpired access
- b.mu.Lock()
- authURL := b.authURL
- keyExpired := b.keyExpired
- b.mu.Unlock()
- return b.Prefs().LoggedOut() || authURL != "" || keyExpired
- }
- // needsMachineAuth returns true if the user has logged in but the machine is not yet authorized.
- // This includes the case where we have a netmap but no valid SelfNode yet (empty netmap after initial login).
- func needsMachineAuth(b *LocalBackend) bool {
- // Note: b.NetMap() and b.Prefs() handle their own locking
- nm := b.NetMap()
- prefs := b.Prefs()
- if prefs.LoggedOut() || nm == nil {
- return false
- }
- // If we have a valid SelfNode, check its MachineAuthorized status
- if nm.SelfNode.Valid() {
- return !nm.SelfNode.MachineAuthorized()
- }
- // Empty netmap (no SelfNode yet) after login also means we need machine auth
- return true
- }
- // hasAuthURL returns true if an authentication URL is present (user needs to visit a URL).
- func hasAuthURL(b *LocalBackend) bool {
- b.mu.Lock()
- authURL := b.authURL
- b.mu.Unlock()
- return authURL != ""
- }
- // canRouteTraffic returns true if the backend is capable of routing traffic.
- // This requires a valid netmap, machine authorization, and WantRunning preference.
- func canRouteTraffic(b *LocalBackend) bool {
- // Note: b.NetMap() and b.Prefs() handle their own locking
- nm := b.NetMap()
- prefs := b.Prefs()
- return nm != nil &&
- nm.SelfNode.Valid() &&
- nm.SelfNode.MachineAuthorized() &&
- prefs.WantRunning()
- }
- // isFullyAuthenticated returns true if the user has completed login and no auth URL is pending.
- func isFullyAuthenticated(b *LocalBackend) bool {
- // Note: b.Prefs() handles its own locking, so we lock only for authURL access
- b.mu.Lock()
- authURL := b.authURL
- b.mu.Unlock()
- return !b.Prefs().LoggedOut() && authURL == ""
- }
- // isWantRunning returns true if the WantRunning preference is set.
- func isWantRunning(b *LocalBackend) bool {
- return b.Prefs().WantRunning()
- }
- // isLoggedIn returns true if the user is logged in (not logged out).
- func isLoggedIn(b *LocalBackend) bool {
- return !b.Prefs().LoggedOut()
- }
|