154 lines
4.9 KiB
Go
154 lines
4.9 KiB
Go
package blocker
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/netip"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/client-go/kubernetes/fake"
|
|
|
|
"git.tornberg.me/go-otel/internal/honeypot"
|
|
)
|
|
|
|
type feed []honeypot.IP
|
|
|
|
func (f feed) PromotedIPs() []honeypot.IP { return f }
|
|
|
|
func setup(t *testing.T, cfg Config, data map[string]string, src Source) (*Blocker, *fake.Clientset, *time.Time) {
|
|
t.Helper()
|
|
client := fake.NewSimpleClientset(&corev1.ConfigMap{
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: "ingress-nginx", Name: "ingress-nginx-controller"},
|
|
Data: data,
|
|
})
|
|
b := New(cfg, client, src)
|
|
clock := time.Date(2026, 9, 19, 12, 0, 0, 0, time.UTC)
|
|
b.now = func() time.Time { return clock }
|
|
return b, client, &clock
|
|
}
|
|
|
|
func current(t *testing.T, c *fake.Clientset) *corev1.ConfigMap {
|
|
t.Helper()
|
|
cm, err := c.CoreV1().ConfigMaps("ingress-nginx").Get(context.Background(), "ingress-nginx-controller", metav1.GetOptions{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return cm
|
|
}
|
|
|
|
func scanner(ip string) honeypot.IP { return honeypot.IP{Address: ip, Count: 40, Paths: 12} }
|
|
|
|
func liveConfig() Config {
|
|
c := DefaultConfig()
|
|
c.DryRun = false
|
|
return c
|
|
}
|
|
|
|
func TestDryRunWritesNothing(t *testing.T) {
|
|
b, client, _ := setup(t, DefaultConfig(), map[string]string{"use-gzip": "true"}, feed{scanner("203.0.113.5")})
|
|
if err := b.Tick(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cm := current(t, client)
|
|
if _, ok := cm.Data["block-cidrs"]; ok || cm.Annotations[AnnotationManaged] != "" {
|
|
t.Fatalf("dry run wrote to the configmap: %+v %+v", cm.Data, cm.Annotations)
|
|
}
|
|
if len(b.last.Candidates) != 1 {
|
|
t.Fatalf("dry run should still report candidates: %+v", b.last)
|
|
}
|
|
}
|
|
|
|
func TestBlocksScannerAndPreservesManualEntries(t *testing.T) {
|
|
b, client, _ := setup(t, liveConfig(), map[string]string{"block-cidrs": "198.51.100.0/24", "use-gzip": "true"}, feed{scanner("203.0.113.5")})
|
|
if err := b.Tick(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cm := current(t, client)
|
|
if got := cm.Data["block-cidrs"]; got != "198.51.100.0/24,203.0.113.5" {
|
|
t.Fatalf("block-cidrs = %q", got)
|
|
}
|
|
if cm.Data["use-gzip"] != "true" {
|
|
t.Fatal("unrelated keys must be untouched")
|
|
}
|
|
var state map[string]int64
|
|
if err := json.Unmarshal([]byte(cm.Annotations[AnnotationManaged]), &state); err != nil || len(state) != 1 {
|
|
t.Fatalf("annotation = %q (%v)", cm.Annotations[AnnotationManaged], err)
|
|
}
|
|
}
|
|
|
|
func TestNeverBlocksInternalOrAllowlisted(t *testing.T) {
|
|
cfg := liveConfig()
|
|
cfg.Allow = []netip.Prefix{netip.MustParsePrefix("203.0.113.0/24")}
|
|
b, client, _ := setup(t, cfg, nil, feed{scanner("127.0.0.1"), scanner("10.244.4.1"), scanner("192.168.1.9"), scanner("203.0.113.77")})
|
|
if err := b.Tick(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if v := current(t, client).Data["block-cidrs"]; v != "" {
|
|
t.Fatalf("blocked %q", v)
|
|
}
|
|
}
|
|
|
|
func TestThresholdsKeepOutOccasionalVisitors(t *testing.T) {
|
|
b, client, _ := setup(t, liveConfig(), nil, feed{
|
|
{Address: "203.0.113.1", Count: 4, Paths: 4}, // too few hits
|
|
{Address: "203.0.113.2", Count: 500, Paths: 1}, // one path hammered
|
|
{Address: "203.0.113.3", Count: 200, Paths: 60}, // broad scanner
|
|
})
|
|
if err := b.Tick(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if v := current(t, client).Data["block-cidrs"]; v != "203.0.113.3" {
|
|
t.Fatalf("block-cidrs = %q", v)
|
|
}
|
|
}
|
|
|
|
func TestExpiryRemovesOnlyManagedEntries(t *testing.T) {
|
|
src := feed{scanner("203.0.113.5")}
|
|
b, client, clock := setup(t, liveConfig(), map[string]string{"block-cidrs": "198.51.100.7"}, src)
|
|
if err := b.Tick(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b.source = feed{} // scanner goes quiet
|
|
*clock = clock.Add(25 * time.Hour)
|
|
if err := b.Tick(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if v := current(t, client).Data["block-cidrs"]; v != "198.51.100.7" {
|
|
t.Fatalf("after expiry block-cidrs = %q, want only the manual entry", v)
|
|
}
|
|
}
|
|
|
|
func TestWritesAreRateLimitedAndNewPerTickCapped(t *testing.T) {
|
|
cfg := liveConfig()
|
|
cfg.MaxNewPerTick = 2
|
|
var many feed
|
|
for _, ip := range []string{"203.0.113.1", "203.0.113.2", "203.0.113.3", "203.0.113.4"} {
|
|
many = append(many, scanner(ip))
|
|
}
|
|
b, client, clock := setup(t, cfg, nil, many)
|
|
if err := b.Tick(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n := len(strings.Split(current(t, client).Data["block-cidrs"], ",")); n != 2 {
|
|
t.Fatalf("first tick added %d, want 2", n)
|
|
}
|
|
*clock = clock.Add(time.Minute) // inside MinWrite
|
|
if err := b.Tick(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n := len(strings.Split(current(t, client).Data["block-cidrs"], ",")); n != 2 {
|
|
t.Fatalf("write was not rate limited: %d entries", n)
|
|
}
|
|
*clock = clock.Add(10 * time.Minute)
|
|
if err := b.Tick(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n := len(strings.Split(current(t, client).Data["block-cidrs"], ",")); n != 4 {
|
|
t.Fatalf("after the interval got %d entries, want 4", n)
|
|
}
|
|
}
|