feat: implement domain resolution via DoH in refresh pipeline. Enhance the collectModulePrefixRows function to resolve domain IPs using DNS over HTTPS (DoH) profiles, adding support for both A and AAAA record types. Introduce new helper functions for handling DoH requests and processing responses, improving domain management and IP prefix aggregation.
CI / changes (push) Successful in 6s
CI / openapi (push) Has been skipped
CI / go (push) Successful in 25s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Successful in 17s
CI / docker-go-prime (push) Successful in 23s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m1s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 2m13s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m23s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m24s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m22s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m7s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been cancelled
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been cancelled

This commit is contained in:
Denozordec
2026-04-06 16:59:59 +07:00
parent 42d956d109
commit 678bd27085
+150 -2
View File
@@ -6,7 +6,9 @@ import (
"encoding/json"
"fmt"
"io"
"net/netip"
"net/http"
"net/url"
"os"
"sort"
"strconv"
@@ -197,15 +199,161 @@ func collectModulePrefixRows(ctx context.Context, st store.Backend, hc *http.Cli
}
return rows, nil
case "DOMAINS":
if _, err := st.ListDomainEntries(tenantID, moduleID); err != nil {
entries, err := st.ListDomainEntries(tenantID, moduleID)
if err != nil {
return nil, err
}
return nil, nil
var profile *store.DohProfile
if mod.DohProfileID != nil && strings.TrimSpace(*mod.DohProfileID) != "" {
profile, err = st.GetDohProfile(tenantID, strings.TrimSpace(*mod.DohProfileID))
if err != nil {
return nil, fmt.Errorf("get doh profile: %w", err)
}
}
var rows []store.PrefixRow
seen := make(map[string]struct{})
for _, e := range entries {
if e == nil {
continue
}
comm := e.CommunityID
if comm == nil && mod.DefaultCommunityID != nil {
c := *mod.DefaultCommunityID
comm = &c
}
addrs, err := resolveDomainIPs(ctx, hc, profile, e.FQDN)
if err != nil {
return nil, fmt.Errorf("resolve domain %q: %w", e.FQDN, err)
}
src := "domain:" + strings.TrimSpace(e.FQDN)
for _, ip := range addrs {
cidr := ipToHostPrefix(ip)
if cidr == "" {
continue
}
key := cidr + "|" + src
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
rows = append(rows, store.PrefixRow{
Prefix: cidr,
CommunityID: comm,
Source: src,
})
}
}
return rows, nil
default:
return nil, fmt.Errorf("unknown module type %q", mod.Type)
}
}
type dohJSONAnswer struct {
Type int `json:"type"`
Data string `json:"data"`
}
type dohJSONResponse struct {
Answer []dohJSONAnswer `json:"Answer"`
}
func resolveDomainIPs(ctx context.Context, hc *http.Client, profile *store.DohProfile, fqdn string) ([]netip.Addr, error) {
host := strings.TrimSpace(strings.TrimSuffix(fqdn, "."))
if host == "" {
return nil, nil
}
if profile == nil || strings.TrimSpace(profile.URL) == "" {
return nil, nil
}
timeout := 10 * time.Second
if profile.TimeoutMs != nil && *profile.TimeoutMs > 0 {
timeout = time.Duration(*profile.TimeoutMs) * time.Millisecond
}
dctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
// RFC8484 endpoint with JSON mode: ?name=<fqdn>&type=A/AAAA
v4, err4 := resolveDomainWithDOHJSON(dctx, hc, strings.TrimSpace(profile.URL), host, "A")
v6, err6 := resolveDomainWithDOHJSON(dctx, hc, strings.TrimSpace(profile.URL), host, "AAAA")
if err4 != nil && err6 != nil {
return nil, fmt.Errorf("doh failed for A and AAAA: %v; %v", err4, err6)
}
return uniqAddrs(append(v4, v6...)), nil
}
func resolveDomainWithDOHJSON(ctx context.Context, hc *http.Client, baseURL, host, qtype string) ([]netip.Addr, error) {
u, err := url.Parse(baseURL)
if err != nil {
return nil, err
}
q := u.Query()
q.Set("name", host)
q.Set("type", qtype)
u.RawQuery = q.Encode()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/dns-json")
resp, err := hc.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
return nil, fmt.Errorf("doh status %s: %s", resp.Status, strings.TrimSpace(string(body)))
}
var payload dohJSONResponse
if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&payload); err != nil {
return nil, err
}
var out []netip.Addr
for _, ans := range payload.Answer {
if (qtype == "A" && ans.Type != 1) || (qtype == "AAAA" && ans.Type != 28) {
continue
}
ip, err := netip.ParseAddr(strings.TrimSpace(ans.Data))
if err != nil {
continue
}
out = append(out, ip.Unmap())
}
return uniqAddrs(out), nil
}
func uniqAddrs(in []netip.Addr) []netip.Addr {
seen := make(map[string]struct{}, len(in))
out := make([]netip.Addr, 0, len(in))
for _, a := range in {
if !a.IsValid() {
continue
}
k := a.String()
if _, ok := seen[k]; ok {
continue
}
seen[k] = struct{}{}
out = append(out, a)
}
return out
}
func ipToHostPrefix(ip netip.Addr) string {
if !ip.IsValid() {
return ""
}
bits := 128
if ip.Is4() {
bits = 32
}
return netip.PrefixFrom(ip, bits).Masked().String()
}
// aggregateTenantPrefixRows builds the union of materialized prefixes for all enabled modules.
// The module that triggered refresh contributes freshRows; every other module is collected live from the store
// (same logic as refresh). We do not reuse other modules' saved revisions as prefix sources, because each revision