mavpoll refuses the flags that would kill it later (V-611)

Three ways a mavpoll process died or lied after start:

- -interval 0 panicked time.NewTicker on the first tick.
- -timeout 0 is 'no deadline' to http.Client, so one wedged source
  stalls every source behind it forever.
- -wg-cmd '' indexed field 0 of an empty slice in pollWg.

All three are now refused in run(), where the operator reads the
message, and pollWg guards its own command as well.

A body that hits maxBodyBytes was silently truncated. A cut kuma page
parses cleanly up to the cut and every monitor past it looks deleted,
so the poller would write 'unknown' over live services and the
down-rule would go quiet. Read one byte past the cap and refuse.

wg's stderr was dropped by Output(), leaving 'exit status 1' in the log
where the real cause is a missing CAP_NET_ADMIN or a bad interface.
This commit is contained in:
2026-08-06 04:03:48 +04:00
parent 0b1efe4911
commit dd699b706f
2 changed files with 91 additions and 1 deletions
+57
View File
@@ -3,6 +3,7 @@ package main
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"os"
@@ -212,3 +213,59 @@ func TestRunRequiresSomethingToPoll(t *testing.T) {
t.Errorf("err = %v, want a 'nothing to poll' refusal", err)
}
}
// A flag value that would kill the poller later is refused at start, before it
// dials core: a non-positive interval panics time.NewTicker on the first tick, a
// zero timeout means http.Client waits forever, and an empty wg command used to
// index field 0 of an empty slice.
func TestRunRefusesFlagsThatCrashLater(t *testing.T) {
cases := []struct {
name string
args []string
want string
}{
{"zero interval", []string{"-interval", "0"}, "-interval must be positive"},
{"negative interval", []string{"-interval", "-5s"}, "-interval must be positive"},
{"zero timeout", []string{"-timeout", "0"}, "-timeout must be positive"},
{"empty wg command", []string{"-wg", "wg0", "-wg-cmd", " "}, "-wg-cmd is empty"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
args := append([]string{"-socket", "/tmp/nope.sock"}, c.args...)
err := run(args)
if err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("err = %v, want %q", err, c.want)
}
})
}
}
// pollWg refuses an empty command rather than panicking on fields[0].
func TestPollWgEmptyCommand(t *testing.T) {
p := &poller{core: &factCore{}, wgIface: "wg0", wgCmd: ""}
if err := p.pollWg(context.Background()); err == nil {
t.Error("want an error, got a poll that ran something")
}
}
// A body at the cap is a truncated body, and a truncated kuma page reads as
// "every monitor past the cut was deleted". Refuse it instead of parsing it.
func TestGetRefusesTruncatedBody(t *testing.T) {
big := strings.Repeat("x", maxBodyBytes+64)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, big)
}))
defer srv.Close()
p := &poller{http: srv.Client()}
if _, err := p.get(context.Background(), srv.URL, ""); err == nil {
t.Error("want an over-size refusal, got a silently truncated body")
}
small := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "ok")
}))
defer small.Close()
body, err := p.get(context.Background(), small.URL, "")
if err != nil || string(body) != "ok" {
t.Errorf("get = %q, %v; want the whole small body", body, err)
}
}