Files
kyle 447842f860 p1.1: dynamic self-IP deny (DDNS/trace, keep previous, atomic swap)
Discover WAN via ddns:<host> then trace:<url> before listen. Persist to
self_ip.json. Previous addresses stay denied for keep_previous. Failure
keeps the last known set. /healthz reports age and source.
2026-09-22 22:59:08 -07:00

169 lines
4.1 KiB
Go

package selfip
import (
"context"
"encoding/json"
"errors"
"net/netip"
"os"
"path/filepath"
"testing"
"time"
"git.wntrmute.dev/kyle/sift/internal/guard"
)
func TestAThenBBothDeniedUntilKeepExpires(t *testing.T) {
g := &guard.Guard{}
now := time.Date(2026, 9, 22, 12, 0, 0, 0, time.UTC)
dir := t.TempDir()
a := netip.MustParseAddr("8.8.8.8")
b := netip.MustParseAddr("1.1.1.1")
cur := a
d := &Discoverer{
Cfg: Config{
Sources: []string{"ddns:wan.example"},
KeepPrevious: 24 * time.Hour,
Path: filepath.Join(dir, "self_ip.json"),
StartupWait: time.Second,
},
Guard: g,
Lookup: func(ctx context.Context, host string) ([]netip.Addr, error) {
return []netip.Addr{cur}, nil
},
}
d.now = func() time.Time { return now }
if err := d.RunOnce(context.Background()); err != nil {
t.Fatal(err)
}
if err := g.CheckAddr(a); err == nil {
t.Fatal("A should be denied")
}
if err := g.CheckAddr(b); err != nil {
t.Fatalf("B should still pass: %v", err)
}
now = now.Add(time.Minute)
cur = b
if err := d.RunOnce(context.Background()); err != nil {
t.Fatal(err)
}
if err := g.CheckAddr(a); err == nil {
t.Fatal("A should still be denied during keep_previous")
}
if err := g.CheckAddr(b); err == nil {
t.Fatal("B should be denied")
}
now = now.Add(25 * time.Hour)
if err := d.RunOnce(context.Background()); err != nil {
t.Fatal(err)
}
if err := g.CheckAddr(a); err != nil {
t.Fatalf("A should have aged out: %v", err)
}
if err := g.CheckAddr(b); err == nil {
t.Fatal("B should still be denied")
}
}
func TestNonGlobalIgnored(t *testing.T) {
g := &guard.Guard{}
d := &Discoverer{
Cfg: Config{Sources: []string{"ddns:wan.example"}, KeepPrevious: time.Hour},
Guard: g,
Lookup: func(ctx context.Context, host string) ([]netip.Addr, error) {
return []netip.Addr{
netip.MustParseAddr("10.0.0.1"),
netip.MustParseAddr("127.0.0.1"),
netip.MustParseAddr("169.254.1.1"),
}, nil
},
}
d.now = time.Now
if err := d.RunOnce(context.Background()); err == nil {
t.Fatal("want failure when every answer is non-global")
}
if p := g.SelfDeny(); len(p) != 0 {
t.Fatalf("self deny = %v, want empty", p)
}
}
func TestFailureKeepsPersisted(t *testing.T) {
g := &guard.Guard{}
dir := t.TempDir()
path := filepath.Join(dir, "self_ip.json")
now := time.Date(2026, 9, 22, 12, 0, 0, 0, time.UTC)
persist := file{Entries: []Entry{{
Addr: "9.9.9.9",
Source: "ddns:wan.example",
Seen: now,
}}}
b, _ := json.Marshal(persist)
if err := os.WriteFile(path, b, 0o640); err != nil {
t.Fatal(err)
}
d := &Discoverer{
Cfg: Config{
Sources: []string{"ddns:wan.example"},
KeepPrevious: 24 * time.Hour,
Path: path,
},
Guard: g,
Lookup: func(ctx context.Context, host string) ([]netip.Addr, error) {
return nil, errors.New("nxdomain")
},
}
d.now = func() time.Time { return now }
if err := d.Load(); err != nil {
t.Fatal(err)
}
if err := g.CheckAddr(netip.MustParseAddr("9.9.9.9")); err == nil {
t.Fatal("persisted addr should be denied after load")
}
if err := d.RunOnce(context.Background()); err == nil {
t.Fatal("want discovery error")
}
if d.Failures.Load() == 0 {
t.Fatal("want a failure count")
}
if err := g.CheckAddr(netip.MustParseAddr("9.9.9.9")); err == nil {
t.Fatal("persisted addr must survive a failed refresh")
}
}
func TestParseTrace(t *testing.T) {
a, err := parseTrace("fl=x\nip=1.1.1.1\nts=1\n")
if err != nil {
t.Fatal(err)
}
if a.String() != "1.1.1.1" {
t.Fatalf("got %s", a)
}
if _, err := parseTrace("nope"); err == nil {
t.Fatal("want error")
}
}
func TestTraceGetter(t *testing.T) {
g := &guard.Guard{}
d := &Discoverer{
Cfg: Config{Sources: []string{"trace:https://1.1.1.1/cdn-cgi/trace"}, KeepPrevious: time.Hour},
Guard: g,
Get: func(ctx context.Context, rawURL string) ([]byte, error) {
if rawURL != "https://1.1.1.1/cdn-cgi/trace" {
t.Fatalf("url %s", rawURL)
}
return []byte("ip=8.8.4.4\n"), nil
},
}
d.now = time.Now
if err := d.RunOnce(context.Background()); err != nil {
t.Fatal(err)
}
if err := g.CheckAddr(netip.MustParseAddr("8.8.4.4")); err == nil {
t.Fatal("want denied")
}
}