Fail the reload when a config entry is skipped with a warning (#1952)

This commit is contained in:
Nate Brown authored and GitHub committed 2026-10-15 23:22:47 -05:00
1 parent 316bf87780
commit a203d044bb
7 files changed
+91 -13

No files matched your search

+7 -5
View File
@@ -316,9 +316,9 @@ type cachedPacketMetrics struct {
func NewHostMapFromConfig(l *slog.Logger, c *config.C) *HostMap { func NewHostMapFromConfig(l *slog.Logger, c *config.C) *HostMap {
hm := newHostMap(l) hm := newHostMap(l)
hm.reload(c, true) hm.reload(c, true, nil)
c.RegisterReloadCallback(func(c *config.C, _ *config.Reload) { c.RegisterReloadCallback(func(c *config.C, r *config.Reload) {
hm.reload(c, false) hm.reload(c, false, r)
}) })
l.Info("Main HostMap created", "preferredRanges", hm.GetPreferredRanges()) l.Info("Main HostMap created", "preferredRanges", hm.GetPreferredRanges())
@@ -337,7 +337,8 @@ func newHostMap(l *slog.Logger) *HostMap {
} }
} }
func (hm *HostMap) reload(c *config.C, initial bool) { // r is nil on the initial load
func (hm *HostMap) reload(c *config.C, initial bool, r *config.Reload) {
if initial || c.HasChanged("preferred_ranges") { if initial || c.HasChanged("preferred_ranges") {
var preferredRanges []netip.Prefix var preferredRanges []netip.Prefix
rawPreferredRanges := c.GetStringSlice("preferred_ranges", []string{}) rawPreferredRanges := c.GetStringSlice("preferred_ranges", []string{})
@@ -348,8 +349,9 @@ func (hm *HostMap) reload(c *config.C, initial bool) {
if err != nil { if err != nil {
hm.l.Warn("Failed to parse preferred ranges, ignoring", hm.l.Warn("Failed to parse preferred ranges, ignoring",
"error", err, "error", err,
"range", rawPreferredRanges, "range", rawPreferredRange,
) )
r.Failed()
continue continue
} }
+13
View File
@@ -447,3 +447,16 @@ func TestHostInfo_markOutOnly(t *testing.T) {
assert.True(t, h.markOut(6), "a relayed send must not consume the epoch edge") assert.True(t, h.markOut(6), "a relayed send must not consume the epoch edge")
assert.False(t, h.markOut(6)) assert.False(t, h.markOut(6))
} }
// A preferred range that doesn't parse is skipped and the rest are used, the reload says one was skipped
func TestHostMap_reloadBadPreferredRangeFails(t *testing.T) {
l := test.NewLogger()
c := config.NewC(l)
require.NoError(t, c.LoadString("preferred_ranges: [10.0.0.0/8]\n"))
hm := NewHostMapFromConfig(l, c)
res := c.ReloadConfigString("preferred_ranges: [10.0.0.0/8, nope]\n")
require.NoError(t, res.Err)
assert.True(t, res.Failed)
assert.Equal(t, []netip.Prefix{netip.MustParsePrefix("10.0.0.0/8")}, hm.GetPreferredRanges())
}
+6 -3
View File
@@ -138,14 +138,14 @@ func NewLightHouseFromConfig(ctx context.Context, l *slog.Logger, c *config.C, c
h.metrics = newLighthouseMetrics() h.metrics = newLighthouseMetrics()
} }
err := h.reload(c, true) err := h.reload(c, true, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
c.RegisterReloadCallback(func(c *config.C, r *config.Reload) { c.RegisterReloadCallback(func(c *config.C, r *config.Reload) {
r.RestartRequiredIfChanged("lighthouse.am_lighthouse", "listen.port", "handshakes.query_buffer", "stats.lighthouse_metrics") r.RestartRequiredIfChanged("lighthouse.am_lighthouse", "listen.port", "handshakes.query_buffer", "stats.lighthouse_metrics")
err := h.reload(c, false) err := h.reload(c, false, r)
switch v := err.(type) { switch v := err.(type) {
case *util.ContextualError: case *util.ContextualError:
v.Log(l) v.Log(l)
@@ -193,7 +193,8 @@ func (lh *LightHouse) GetUpdateInterval() int64 {
return lh.interval.Load() return lh.interval.Load()
} }
func (lh *LightHouse) reload(c *config.C, initial bool) error { // r is nil on the initial load
func (lh *LightHouse) reload(c *config.C, initial bool, r *config.Reload) error {
if initial || c.HasChanged("lighthouse.advertise_addrs") { if initial || c.HasChanged("lighthouse.advertise_addrs") {
rawAdvAddrs := c.GetStringSlice("lighthouse.advertise_addrs", []string{}) rawAdvAddrs := c.GetStringSlice("lighthouse.advertise_addrs", []string{})
advAddrs := make([]netip.AddrPort, 0) advAddrs := make([]netip.AddrPort, 0)
@@ -228,6 +229,7 @@ func (lh *LightHouse) reload(c *config.C, initial bool) error {
"addr", rawAddr, "addr", rawAddr,
"entry", i+1, "entry", i+1,
) )
r.Failed()
continue continue
} }
@@ -386,6 +388,7 @@ func (lh *LightHouse) reload(c *config.C, initial bool) error {
"relay", v, "relay", v,
"error", err, "error", err,
) )
r.Failed()
} else { } else {
lh.l.Info("Read relay from config", "relay", v) lh.l.Info("Read relay from config", "relay", v)
relaysForMe = append(relaysForMe, configRIP) relaysForMe = append(relaysForMe, configRIP)
+29 -1
View File
@@ -296,7 +296,7 @@ func TestLighthouse_reload(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
c.ReloadConfigString(string(rc)) c.ReloadConfigString(string(rc))
err = lh.reload(c, false) err = lh.reload(c, false, nil)
require.NoError(t, err) require.NoError(t, err)
} }
@@ -1014,3 +1014,31 @@ func TestLighthouse_reloadRestartRequired(t *testing.T) {
require.NoError(t, res.Err) require.NoError(t, res.Err)
assert.True(t, res.RestartRequired) assert.True(t, res.RestartRequired)
} }
// A relay that doesn't parse is skipped and the rest are used, the reload says one was skipped
func TestLighthouse_reloadBadRelayFails(t *testing.T) {
l := test.NewLogger()
c := config.NewC(l)
require.NoError(t, c.LoadString("listen:\n port: 4242\n"))
lh, err := NewLightHouseFromConfig(t.Context(), l, c, testCertState(netip.MustParsePrefix("10.128.0.1/24")), nil, nil)
require.NoError(t, err)
res := c.ReloadConfigString("listen:\n port: 4242\nrelay:\n relays: [10.128.0.2, nope]\n")
require.NoError(t, res.Err)
assert.True(t, res.Failed)
assert.Equal(t, []netip.Addr{netip.MustParseAddr("10.128.0.2")}, lh.GetRelaysForMe())
}
// An advertise_addrs entry inside our own network is skipped and the rest are used, the reload says one was skipped
func TestLighthouse_reloadAdvertiseAddrInNetworkFails(t *testing.T) {
l := test.NewLogger()
c := config.NewC(l)
require.NoError(t, c.LoadString("listen:\n port: 4242\n"))
lh, err := NewLightHouseFromConfig(t.Context(), l, c, testCertState(netip.MustParsePrefix("10.128.0.1/24")), nil, nil)
require.NoError(t, err)
res := c.ReloadConfigString("listen:\n port: 4242\nlighthouse:\n advertise_addrs: [\"1.2.3.4:4242\", \"10.128.0.5:4242\"]\n")
require.NoError(t, res.Err)
assert.True(t, res.Failed)
assert.Equal(t, []netip.AddrPort{netip.MustParseAddrPort("1.2.3.4:4242")}, lh.GetAdvertiseAddrs())
}
+1 -1
View File
@@ -78,7 +78,7 @@ func Main(c *config.C, configTest bool, buildVersion string, l *slog.Logger, dev
wireSSHReload(l, ssh, c) wireSSHReload(l, ssh, c)
var sshStart func() var sshStart func()
if c.GetBool("sshd.enabled", false) { if c.GetBool("sshd.enabled", false) {
sshStart, err = configSSH(l, ssh, c) sshStart, err = configSSH(l, ssh, c, nil)
if err != nil { if err != nil {
l.Warn("Failed to configure sshd, ssh debugging will not be available", "error", err) l.Warn("Failed to configure sshd, ssh debugging will not be available", "error", err)
sshStart = nil sshStart = nil
+10 -2
View File
@@ -78,7 +78,7 @@ func wireSSHReload(l *slog.Logger, ssh *sshd.SSHServer, c *config.C) {
} }
return return
} }
sshRun, err := configSSH(l, ssh, c) sshRun, err := configSSH(l, ssh, c, r)
if err != nil { if err != nil {
l.Error("Failed to reconfigure the sshd", "error", err) l.Error("Failed to reconfigure the sshd", "error", err)
ssh.Stop() ssh.Stop()
@@ -117,7 +117,8 @@ func loadSSHHostKey(c *config.C) ([]byte, error) {
// updates the passed-in SSHServer. On success, it returns a function // updates the passed-in SSHServer. On success, it returns a function
// that callers may invoke to run the configured ssh server. On // that callers may invoke to run the configured ssh server. On
// failure, it returns nil, error. // failure, it returns nil, error.
func configSSH(l *slog.Logger, ssh *sshd.SSHServer, c *config.C) (func(), error) { // r is nil at startup, entries that are skipped with a warning fail it on a reload
func configSSH(l *slog.Logger, ssh *sshd.SSHServer, c *config.C, r *config.Reload) (func(), error) {
listen := c.GetString("sshd.listen", "") listen := c.GetString("sshd.listen", "")
if listen == "" { if listen == "" {
return nil, fmt.Errorf("sshd.listen must be provided") return nil, fmt.Errorf("sshd.listen must be provided")
@@ -150,6 +151,7 @@ func configSSH(l *slog.Logger, ssh *sshd.SSHServer, c *config.C) (func(), error)
err := ssh.AddTrustedCA(caAuthorizedKey) err := ssh.AddTrustedCA(caAuthorizedKey)
if err != nil { if err != nil {
l.Warn("SSH CA had an error, ignoring", "error", err, "sshCA", caAuthorizedKey) l.Warn("SSH CA had an error, ignoring", "error", err, "sshCA", caAuthorizedKey)
r.Failed()
continue continue
} }
} }
@@ -161,12 +163,14 @@ func configSSH(l *slog.Logger, ssh *sshd.SSHServer, c *config.C) (func(), error)
kDef, ok := rk.(map[string]any) kDef, ok := rk.(map[string]any)
if !ok { if !ok {
l.Warn("Authorized user had an error, ignoring", "sshKeyConfig", rk) l.Warn("Authorized user had an error, ignoring", "sshKeyConfig", rk)
r.Failed()
continue continue
} }
user, ok := kDef["user"].(string) user, ok := kDef["user"].(string)
if !ok { if !ok {
l.Warn("Authorized user is missing the user field", "sshKeyConfig", rk) l.Warn("Authorized user is missing the user field", "sshKeyConfig", rk)
r.Failed()
continue continue
} }
@@ -180,6 +184,7 @@ func configSSH(l *slog.Logger, ssh *sshd.SSHServer, c *config.C) (func(), error)
"sshKeyConfig", rk, "sshKeyConfig", rk,
"sshKey", v, "sshKey", v,
) )
r.Failed()
continue continue
} }
@@ -191,6 +196,7 @@ func configSSH(l *slog.Logger, ssh *sshd.SSHServer, c *config.C) (func(), error)
"sshKeyConfig", rk, "sshKeyConfig", rk,
"sshKey", subK, "sshKey", subK,
) )
r.Failed()
continue continue
} }
@@ -200,12 +206,14 @@ func configSSH(l *slog.Logger, ssh *sshd.SSHServer, c *config.C) (func(), error)
"error", err, "error", err,
"sshKeyConfig", sk, "sshKeyConfig", sk,
) )
r.Failed()
continue continue
} }
} }
default: default:
l.Warn("Authorized user is missing the keys field or was not understood", "sshKeyConfig", rk) l.Warn("Authorized user is missing the keys field or was not understood", "sshKeyConfig", rk)
r.Failed()
} }
} }
} else { } else {
+25 -1
View File
@@ -47,7 +47,7 @@ func newSSHDReloadHarness(t *testing.T) *sshdReloadHarness {
h.server, err = sshd.NewSSHServer(t.Context(), l) h.server, err = sshd.NewSSHServer(t.Context(), l)
require.NoError(t, err) require.NoError(t, err)
wireSSHReload(l, h.server, h.c) wireSSHReload(l, h.server, h.c)
run, err := configSSH(l, h.server, h.c) run, err := configSSH(l, h.server, h.c, nil)
require.NoError(t, err) require.NoError(t, err)
go run() go run()
t.Cleanup(h.server.Stop) t.Cleanup(h.server.Stop)
@@ -226,3 +226,27 @@ func TestSSHDReload_BadHostKeyFileStops(t *testing.T) {
assert.True(t, h.c.ReloadConfigString(h.config()).Failed) assert.True(t, h.c.ReloadConfigString(h.config()).Failed)
require.Eventually(t, func() bool { return !h.server.Running() }, 5*time.Second, 10*time.Millisecond) require.Eventually(t, func() bool { return !h.server.Running() }, 5*time.Second, 10*time.Millisecond)
} }
// A trusted CA or authorized user that is skipped with a warning fails the reload, the valid ones are still used
func TestSSHDReload_SkippedEntriesFail(t *testing.T) {
cases := map[string]string{
"bad trusted ca": " trusted_cas: [nope]\n",
"user entry not a map": " - nope\n",
"user missing": " - keys: [nope]\n",
"bad key": " - user: other\n keys: nope\n",
"key in a list not a string": " - user: other\n keys: [5]\n",
"bad key in a list": " - user: other\n keys: [nope]\n",
"keys missing": " - user: other\n",
}
for name, extra := range cases {
t.Run(name, func(t *testing.T) {
h := newSSHDReloadHarness(t)
h.extra = extra
res := h.c.ReloadConfigString(h.config())
require.NoError(t, res.Err)
assert.True(t, res.Failed)
require.Eventually(t, h.server.Running, 5*time.Second, 10*time.Millisecond)
h.connect(t)
})
}
}