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.
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user