feat(lookup): resolve domain to IPs for membership check
CI / changes (push) Successful in 5s
CI / commitlint (push) Skipped
CI / openapi (push) Successful in 28s
CI / web (push) Successful in 1m6s
CI / go (push) Successful in 57s
CI / bird2 (push) Successful in 15s
CI / release (push) Successful in 4m6s
CI / changes (push) Successful in 5s
CI / commitlint (push) Skipped
CI / openapi (push) Successful in 28s
CI / web (push) Successful in 1m6s
CI / go (push) Successful in 57s
CI / bird2 (push) Successful in 15s
CI / release (push) Successful in 4m6s
Для FQDN после проверки DOMAINS выполняется live DNS (A/AAAA), каждый IP проверяется по IP_RANGES и snapshots; в ответе resolved_ips / resolved_ip, UI KPI и OpenAPI обновлены. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
+89
-10
@@ -3,7 +3,9 @@
|
||||
package lookup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"unicode"
|
||||
@@ -49,20 +51,37 @@ type Match struct {
|
||||
CommunityID *string `json:"community_id,omitempty"`
|
||||
Community string `json:"community,omitempty"`
|
||||
CommunityTitle string `json:"community_title,omitempty"`
|
||||
// ResolvedIP is set when the hit came from a DNS-resolved address of a domain query.
|
||||
ResolvedIP string `json:"resolved_ip,omitempty"`
|
||||
}
|
||||
|
||||
// Result is the full lookup response payload.
|
||||
type Result struct {
|
||||
Query string `json:"query"`
|
||||
QueryKind QueryKind `json:"query_kind"`
|
||||
Normalized string `json:"normalized"`
|
||||
Matched bool `json:"matched"`
|
||||
MatchCount int `json:"match_count"`
|
||||
Matches []Match `json:"matches"`
|
||||
Query string `json:"query"`
|
||||
QueryKind QueryKind `json:"query_kind"`
|
||||
Normalized string `json:"normalized"`
|
||||
Matched bool `json:"matched"`
|
||||
MatchCount int `json:"match_count"`
|
||||
Matches []Match `json:"matches"`
|
||||
ResolvedIPs []string `json:"resolved_ips,omitempty"`
|
||||
}
|
||||
|
||||
// DomainResolver resolves a hostname to IP addresses (A/AAAA).
|
||||
type DomainResolver func(ctx context.Context, host string) ([]netip.Addr, error)
|
||||
|
||||
// Lookup checks whether q (IP or FQDN) is present in tenant lists (entries + snapshots).
|
||||
func Lookup(st store.Backend, tenantID, q string) (*Result, error) {
|
||||
// For domains, FQDN membership is checked first, then live DNS resolve and IP membership.
|
||||
func Lookup(ctx context.Context, st store.Backend, tenantID, q string) (*Result, error) {
|
||||
return LookupWithResolver(ctx, st, tenantID, q, systemDNSResolver)
|
||||
}
|
||||
|
||||
// LookupWithResolver is like Lookup but uses resolve for domain→IP (tests / alternate DNS).
|
||||
func LookupWithResolver(
|
||||
ctx context.Context,
|
||||
st store.Backend,
|
||||
tenantID, q string,
|
||||
resolve DomainResolver,
|
||||
) (*Result, error) {
|
||||
raw := strings.TrimSpace(q)
|
||||
if raw == "" {
|
||||
return nil, fmt.Errorf("%w: empty query", store.ErrInvalidInput)
|
||||
@@ -87,7 +106,7 @@ func Lookup(st store.Backend, tenantID, q string) (*Result, error) {
|
||||
if addr, err := netip.ParseAddr(raw); err == nil {
|
||||
out.QueryKind = KindIP
|
||||
out.Normalized = addr.String()
|
||||
if err := lookupIP(st, tenantID, addr, out, commByID); err != nil {
|
||||
if err := lookupIP(st, tenantID, addr, out, commByID, ""); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
@@ -100,6 +119,12 @@ func Lookup(st store.Backend, tenantID, q string) (*Result, error) {
|
||||
if err := lookupDomain(st, tenantID, fqdn, out, commByID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resolve == nil {
|
||||
resolve = systemDNSResolver
|
||||
}
|
||||
if err := lookupResolvedIPs(ctx, st, tenantID, fqdn, out, commByID, resolve); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
out.MatchCount = len(out.Matches)
|
||||
@@ -107,7 +132,59 @@ func Lookup(st store.Backend, tenantID, q string) (*Result, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func lookupIP(st store.Backend, tenantID string, addr netip.Addr, out *Result, commByID map[string]*store.Community) error {
|
||||
func systemDNSResolver(ctx context.Context, host string) ([]netip.Addr, error) {
|
||||
ips, err := net.DefaultResolver.LookupNetIP(ctx, "ip", host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return uniqAddrs(ips), nil
|
||||
}
|
||||
|
||||
func uniqAddrs(in []netip.Addr) []netip.Addr {
|
||||
seen := make(map[netip.Addr]struct{}, len(in))
|
||||
out := make([]netip.Addr, 0, len(in))
|
||||
for _, a := range in {
|
||||
a = a.Unmap()
|
||||
if _, ok := seen[a]; ok {
|
||||
continue
|
||||
}
|
||||
seen[a] = struct{}{}
|
||||
out = append(out, a)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func lookupResolvedIPs(
|
||||
ctx context.Context,
|
||||
st store.Backend,
|
||||
tenantID, fqdn string,
|
||||
out *Result,
|
||||
commByID map[string]*store.Community,
|
||||
resolve DomainResolver,
|
||||
) error {
|
||||
ips, err := resolve(ctx, fqdn)
|
||||
if err != nil {
|
||||
// DNS failure must not hide FQDN-layer matches already collected.
|
||||
return nil
|
||||
}
|
||||
out.ResolvedIPs = make([]string, 0, len(ips))
|
||||
for _, ip := range ips {
|
||||
out.ResolvedIPs = append(out.ResolvedIPs, ip.String())
|
||||
if err := lookupIP(st, tenantID, ip, out, commByID, ip.String()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func lookupIP(
|
||||
st store.Backend,
|
||||
tenantID string,
|
||||
addr netip.Addr,
|
||||
out *Result,
|
||||
commByID map[string]*store.Community,
|
||||
resolvedIP string,
|
||||
) error {
|
||||
for _, mod := range st.ListModules(tenantID) {
|
||||
if mod == nil {
|
||||
continue
|
||||
@@ -137,6 +214,7 @@ func lookupIP(st store.Backend, tenantID string, addr netip.Addr, out *Result, c
|
||||
MatchedValue: e.Prefix,
|
||||
EntryID: e.ID,
|
||||
CommunityID: resolveCommunityID(e.CommunityID, mod.DefaultCommunityID),
|
||||
ResolvedIP: resolvedIP,
|
||||
}, commByID))
|
||||
}
|
||||
}
|
||||
@@ -165,6 +243,7 @@ func lookupIP(st store.Backend, tenantID string, addr netip.Addr, out *Result, c
|
||||
MatchedValue: row.Prefix,
|
||||
Source: row.Source,
|
||||
CommunityID: row.CommunityID,
|
||||
ResolvedIP: resolvedIP,
|
||||
}, commByID))
|
||||
}
|
||||
}
|
||||
@@ -218,7 +297,7 @@ func lookupDomain(st store.Backend, tenantID, fqdn string, out *Result, commByID
|
||||
}
|
||||
out.Matches = append(out.Matches, decorateMatch(Match{
|
||||
Layer: LayerSnapshot,
|
||||
ModuleID: mod.ID,
|
||||
ModuleID: mid,
|
||||
ModuleName: mod.Name,
|
||||
ModuleType: mod.Type,
|
||||
MatchKind: MatchPrefix,
|
||||
|
||||
Reference in New Issue
Block a user