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
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user