Files
go-otel/internal/blocker/blocker_test.go
T
mats f30adf05af
Build and Publish / Metadata (push) Successful in 2s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 1m23s
all the changes
2026-09-19 23:25:59 +02:00

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)
}
}