Compare commits

...

1 Commits

Author SHA1 Message Date
riccardom
576ed4c343 [client] harden inactivity-threshold parsing (CodeRabbit)
- Treat zero/negative durations (0s, -5m) as invalid: fall through to the warning
  and default instead of silently returning nil.
- Guard the legacy bare-integer minutes path against time.Duration overflow via
  strconv.ParseInt bounded by maxInactivityMinutes.
- Extend TestInactivityThresholdEnv with overflow/boundary cases and assert the
  warning is emitted for invalid values.
2026-07-28 22:10:46 +02:00
2 changed files with 32 additions and 15 deletions

View File

@@ -379,6 +379,10 @@ func resolveLazyForce(mdmState lazyconn.State) lazyForce {
}
}
// maxInactivityMinutes is the largest bare-integer minute value that still fits in a
// time.Duration without overflowing.
const maxInactivityMinutes = int64(time.Duration(1<<63-1) / time.Minute)
func inactivityThresholdEnv() *time.Duration {
envValue := os.Getenv(lazyconn.EnvInactivityThreshold)
if envValue == "" {
@@ -387,15 +391,13 @@ func inactivityThresholdEnv() *time.Duration {
// Documented format: a Go duration such as "30m" or "1h".
if d, err := time.ParseDuration(envValue); err == nil {
if d <= 0 {
return nil
if d > 0 {
return &d
}
return &d
}
// Backwards compatibility: a bare integer used to be interpreted as minutes.
if parsedMinutes, err := strconv.Atoi(envValue); err == nil && parsedMinutes > 0 {
d := time.Duration(parsedMinutes) * time.Minute
// Zero/negative durations are invalid; fall through to the warning.
} else if minutes, err := strconv.ParseInt(envValue, 10, 64); err == nil && minutes > 0 && minutes <= maxInactivityMinutes {
// Backwards compatibility: a bare integer used to be interpreted as minutes.
d := time.Duration(minutes) * time.Minute
return &d
}

View File

@@ -5,10 +5,13 @@ import (
"net"
"net/netip"
"os"
"strconv"
"sync"
"testing"
"time"
"github.com/sirupsen/logrus"
logrustest "github.com/sirupsen/logrus/hooks/test"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"github.com/netbirdio/netbird/client/iface/wgaddr"
@@ -106,24 +109,31 @@ func TestConnMgr_ActivatePeerConcurrentWithLifecycle(t *testing.T) {
}
func TestInactivityThresholdEnv(t *testing.T) {
maxMinutes := int64(time.Duration(1<<63-1) / time.Minute)
hook := logrustest.NewGlobal()
tests := []struct {
name string
val string
want *time.Duration
name string
val string
want *time.Duration
wantWarn bool
}{
{name: "unset", val: "", want: nil},
{name: "go duration minutes", val: "30m", want: durPtr(30 * time.Minute)},
{name: "go duration hours", val: "1h", want: durPtr(time.Hour)},
{name: "go duration seconds", val: "90s", want: durPtr(90 * time.Second)},
{name: "bare integer is minutes (backwards compat)", val: "5", want: durPtr(5 * time.Minute)},
{name: "zero duration", val: "0s", want: nil},
{name: "zero integer", val: "0", want: nil},
{name: "negative duration", val: "-5m", want: nil},
{name: "garbage", val: "abc", want: nil},
{name: "max bare minutes accepted", val: strconv.FormatInt(maxMinutes, 10), want: durPtr(time.Duration(maxMinutes) * time.Minute)},
{name: "overflowing bare minutes warns", val: strconv.FormatInt(maxMinutes+1, 10), want: nil, wantWarn: true},
{name: "zero duration warns", val: "0s", want: nil, wantWarn: true},
{name: "zero integer warns", val: "0", want: nil, wantWarn: true},
{name: "negative duration warns", val: "-5m", want: nil, wantWarn: true},
{name: "garbage warns", val: "abc", want: nil, wantWarn: true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
hook.Reset()
t.Setenv(lazyconn.EnvInactivityThreshold, tc.val)
got := inactivityThresholdEnv()
switch {
@@ -134,6 +144,11 @@ func TestInactivityThresholdEnv(t *testing.T) {
case tc.want != nil && *got != *tc.want:
t.Fatalf("want %v, got %v", *tc.want, *got)
}
gotWarn := hook.LastEntry() != nil && hook.LastEntry().Level == logrus.WarnLevel
if gotWarn != tc.wantWarn {
t.Fatalf("warn logged = %v, want %v", gotWarn, tc.wantWarn)
}
})
}
}