package httpapi import ( "context" "encoding/base64" "net" "net/http" "net/http/httptest" "sync/atomic" "testing" "time" "evobgp/internal/httpclient" "evobgp/internal/jobs" "evobgp/internal/pipeline" "evobgp/internal/store" "github.com/miekg/dns" ) func TestBootstrapWorkerPreservesExplicitPrivateDOH(t *testing.T) { t.Setenv("EVOBGP_NODE_DISPATCH_ENABLED", "0") t.Setenv("EVOBGP_BIRDC_SOCKET", "") t.Setenv("EVOBGP_CDN_ALLOW_PRIVATE", "0") var calls atomic.Int32 resolver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls.Add(1) raw, err := base64.RawURLEncoding.DecodeString(r.URL.Query().Get("dns")) if err != nil { w.WriteHeader(400) return } query := new(dns.Msg) if err = query.Unpack(raw); err != nil || len(query.Question) == 0 { w.WriteHeader(400) return } answer := new(dns.Msg) answer.SetReply(query) if query.Question[0].Qtype == dns.TypeA { answer.Answer = []dns.RR{&dns.A{Hdr: dns.RR_Header{Name: query.Question[0].Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 60}, A: net.IPv4(192, 0, 2, 20)}} } body, err := answer.Pack() if err != nil { t.Error(err) return } w.Header().Set("Content-Type", "application/dns-message") _, _ = w.Write(body) })) defer resolver.Close() st, reg, _, err := BootstrapWorkers(context.Background(), Options{SeedDemo: true}) if err != nil { t.Fatal(err) } defer func() { reg.RequestCancelAll() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if e := reg.Drain(ctx); e != nil { t.Error(e) } }() tenant, _, _, _, _ := st.DemoIDs() profile, err := st.CreateDohProfile(tenant, &store.DohProfile{Name: "explicit private resolver", URL: resolver.URL}) if err != nil { t.Fatal(err) } mod, err := st.CreateModule(tenant, &store.Module{Name: "private-doh", Type: "DOMAINS", Enabled: true, DohProfileIDs: []string{profile.ID}}) if err != nil { t.Fatal(err) } if _, err = st.CreateDomainEntry(tenant, mod.ID, &store.DomainEntry{FQDN: "private-resolver.example.test"}); err != nil { t.Fatal(err) } j, _, err := reg.Enqueue(tenant, jobs.KindModuleRefresh, nil, &mod.ID, map[string]any{"module_id": mod.ID}) if err != nil { t.Fatal(err) } deadline := time.After(5 * time.Second) ticker := time.NewTicker(5 * time.Millisecond) defer ticker.Stop() for { snap := j.Snapshot() status := snap["status"] if status == jobs.StatusSucceeded { break } if status == jobs.StatusFailed || status == jobs.StatusCancelled { t.Fatalf("worker refresh failed: %+v", snap) } select { case <-deadline: t.Fatal("worker did not finish") case <-ticker.C: } } if calls.Load() < 2 { t.Fatalf("bootstrap worker blocked private DoH: %d requests", calls.Load()) } // The same general-client choice must not remove CDN's independent ACL. cdn := httptest.NewTLSServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { t.Error("private CDN reached") })) defer cdn.Close() req, err := http.NewRequest(http.MethodGet, cdn.URL, nil) if err != nil { t.Fatal(err) } if _, err = pipeline.UpstreamHTTPDo(context.Background(), httpclient.New(httpclient.DefaultTimeout), req); err == nil { t.Fatal("private CDN accepted") } }