M ipn/ipnlocal/local.go => ipn/ipnlocal/local.go +34 -14
@@ 479,7 479,7 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
mConn.SetNetInfoCallback(b.setNetInfo)
if sys.InitialConfig != nil {
- if err := b.setConfigLocked(sys.InitialConfig); err != nil {
+ if err := b.initPrefsFromConfig(sys.InitialConfig); err != nil {
return nil, err
}
}
@@ 712,8 712,8 @@ func (b *LocalBackend) SetDirectFileRoot(dir string) {
// It returns (false, nil) if not running in declarative mode, (true, nil) on
// success, or (false, error) on failure.
func (b *LocalBackend) ReloadConfig() (ok bool, err error) {
- b.mu.Lock()
- defer b.mu.Unlock()
+ unlock := b.lockAndGetUnlock()
+ defer unlock()
if b.conf == nil {
return false, nil
}
@@ 721,18 721,21 @@ func (b *LocalBackend) ReloadConfig() (ok bool, err error) {
if err != nil {
return false, err
}
- if err := b.setConfigLocked(conf); err != nil {
+ if err := b.setConfigLockedOnEntry(conf, unlock); err != nil {
return false, fmt.Errorf("error setting config: %w", err)
}
return true, nil
}
-func (b *LocalBackend) setConfigLocked(conf *conffile.Config) error {
-
- // TODO(irbekrm): notify the relevant components to consume any prefs
- // updates. Currently only initial configfile settings are applied
- // immediately.
+// initPrefsFromConfig initializes the backend's prefs from the provided config.
+// This should only be called once, at startup. For updates at runtime, use
+// [LocalBackend.setConfigLocked].
+func (b *LocalBackend) initPrefsFromConfig(conf *conffile.Config) error {
+ // TODO(maisem,bradfitz): combine this with setConfigLocked. This is called
+ // before anything is running, so there's no need to lock and we don't
+ // update any subsystems. At runtime, we both need to lock and update
+ // subsystems with the new prefs.
p := b.pm.CurrentPrefs().AsStruct()
mp, err := conf.Parsed.ToPrefs()
if err != nil {
@@ 742,13 745,14 @@ func (b *LocalBackend) setConfigLocked(conf *conffile.Config) error {
if err := b.pm.SetPrefs(p.View(), ipn.NetworkProfile{}); err != nil {
return err
}
+ b.setStaticEndpointsFromConfigLocked(conf)
+ b.conf = conf
+ return nil
+}
- defer func() {
- b.conf = conf
- }()
-
+func (b *LocalBackend) setStaticEndpointsFromConfigLocked(conf *conffile.Config) {
if conf.Parsed.StaticEndpoints == nil && (b.conf == nil || b.conf.Parsed.StaticEndpoints == nil) {
- return nil
+ return
}
// Ensure that magicsock conn has the up to date static wireguard
@@ 762,6 766,22 @@ func (b *LocalBackend) setConfigLocked(conf *conffile.Config) error {
ms.SetStaticEndpoints(views.SliceOf(conf.Parsed.StaticEndpoints))
}
}
+}
+
+// setConfigLockedOnEntry uses the provided config to update the backend's prefs
+// and other state.
+func (b *LocalBackend) setConfigLockedOnEntry(conf *conffile.Config, unlock unlockOnce) error {
+ defer unlock()
+ p := b.pm.CurrentPrefs().AsStruct()
+ mp, err := conf.Parsed.ToPrefs()
+ if err != nil {
+ return fmt.Errorf("error parsing config to prefs: %w", err)
+ }
+ p.ApplyEdits(&mp)
+ b.setStaticEndpointsFromConfigLocked(conf)
+ b.setPrefsLockedOnEntry(p, unlock)
+
+ b.conf = conf
return nil
}
M ipn/ipnlocal/local_test.go => ipn/ipnlocal/local_test.go +51 -8
@@ 13,6 13,7 @@ import (
"net/http"
"net/netip"
"os"
+ "path/filepath"
"reflect"
"slices"
"strings"
@@ 32,6 33,7 @@ import (
"tailscale.com/health"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
+ "tailscale.com/ipn/conffile"
"tailscale.com/ipn/ipnauth"
"tailscale.com/ipn/store/mem"
"tailscale.com/net/netcheck"
@@ 432,16 434,25 @@ func (panicOnUseTransport) RoundTrip(*http.Request) (*http.Response, error) {
}
func newTestLocalBackend(t testing.TB) *LocalBackend {
+ return newTestLocalBackendWithSys(t, new(tsd.System))
+}
+
+// newTestLocalBackendWithSys creates a new LocalBackend with the given tsd.System.
+// If the state store or engine are not set in sys, they will be set to a new
+// in-memory store and fake userspace engine, respectively.
+func newTestLocalBackendWithSys(t testing.TB, sys *tsd.System) *LocalBackend {
var logf logger.Logf = logger.Discard
- sys := new(tsd.System)
- store := new(mem.Store)
- sys.Set(store)
- eng, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker(), sys.UserMetricsRegistry())
- if err != nil {
- t.Fatalf("NewFakeUserspaceEngine: %v", err)
+ if _, ok := sys.StateStore.GetOK(); !ok {
+ sys.Set(new(mem.Store))
+ }
+ if _, ok := sys.Engine.GetOK(); !ok {
+ eng, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker(), sys.UserMetricsRegistry())
+ if err != nil {
+ t.Fatalf("NewFakeUserspaceEngine: %v", err)
+ }
+ t.Cleanup(eng.Close)
+ sys.Set(eng)
}
- t.Cleanup(eng.Close)
- sys.Set(eng)
lb, err := NewLocalBackend(logf, logid.PublicID{}, sys, 0)
if err != nil {
t.Fatalf("NewLocalBackend: %v", err)
@@ 4423,3 4434,35 @@ func TestLoginNotifications(t *testing.T) {
})
}
}
+
+// TestConfigFileReload tests that the LocalBackend reloads its configuration
+// when the configuration file changes.
+func TestConfigFileReload(t *testing.T) {
+ cfg1 := `{"Hostname": "foo", "Version": "alpha0"}`
+ f := filepath.Join(t.TempDir(), "cfg")
+ must.Do(os.WriteFile(f, []byte(cfg1), 0600))
+ sys := new(tsd.System)
+ sys.InitialConfig = must.Get(conffile.Load(f))
+ lb := newTestLocalBackendWithSys(t, sys)
+ must.Do(lb.Start(ipn.Options{}))
+
+ lb.mu.Lock()
+ hn := lb.hostinfo.Hostname
+ lb.mu.Unlock()
+ if hn != "foo" {
+ t.Fatalf("got %q; want %q", hn, "foo")
+ }
+
+ cfg2 := `{"Hostname": "bar", "Version": "alpha0"}`
+ must.Do(os.WriteFile(f, []byte(cfg2), 0600))
+ if !must.Get(lb.ReloadConfig()) {
+ t.Fatal("reload failed")
+ }
+
+ lb.mu.Lock()
+ hn = lb.hostinfo.Hostname
+ lb.mu.Unlock()
+ if hn != "bar" {
+ t.Fatalf("got %q; want %q", hn, "bar")
+ }
+}