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"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/netip"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -197,15 +199,161 @@ func collectModulePrefixRows(ctx context.Context, st store.Backend, hc *http.Cli
|
|||||||
}
|
}
|
||||||
return rows, nil
|
return rows, nil
|
||||||
case "DOMAINS":
|
case "DOMAINS":
|
||||||
if _, err := st.ListDomainEntries(tenantID, moduleID); err != nil {
|
entries, err := st.ListDomainEntries(tenantID, moduleID)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
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:
|
default:
|
||||||
return nil, fmt.Errorf("unknown module type %q", mod.Type)
|
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.
|
// 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
|
// 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
|
// (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