CI / changes (push) Successful in 5s
CI / openapi (push) Has been skipped
CI / go (push) Successful in 24s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m9s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m3s
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Successful in 16s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m0s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 1m37s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m25s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m29s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m25s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m21s
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m24s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m31s
157 lines
4.4 KiB
Go
157 lines
4.4 KiB
Go
// Package asnresolve fetches IP prefixes announced by an ASN (control-plane ingest).
|
|
package asnresolve
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/netip"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// DefaultRIPEStatURL is the RIPEstat announced-prefixes data call (no API key).
|
|
const DefaultRIPEStatURL = "https://stat.ripe.net/data/announced-prefixes/data.json"
|
|
|
|
// DefaultASOverviewURL is the RIPEstat as-overview data call (holder / org name, no API key).
|
|
const DefaultASOverviewURL = "https://stat.ripe.net/data/as-overview/data.json"
|
|
|
|
// AnnouncedPrefixes returns currently announced IPv4/IPv6 prefixes for the ASN (best-effort via RIPEstat).
|
|
func AnnouncedPrefixes(ctx context.Context, hc *http.Client, asn int64) ([]netip.Prefix, error) {
|
|
if hc == nil {
|
|
hc = http.DefaultClient
|
|
}
|
|
base := strings.TrimSpace(os.Getenv("EVOBGP_RIPESTAT_ANNOUNCED_PREFIXES_URL"))
|
|
if base == "" {
|
|
base = DefaultRIPEStatURL
|
|
}
|
|
u := fmt.Sprintf("%s?resource=AS%d", strings.TrimSuffix(base, "?"), asn)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Accept", "application/json")
|
|
req.Header.Set("User-Agent", "evobgp-asnresolve/1.0")
|
|
|
|
resp, err := hc.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ripestat fetch AS%d: %w", asn, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
body, err := io.ReadAll(io.LimitReader(resp.Body, 32<<20))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("ripestat AS%d: HTTP %s: %s", asn, resp.Status, truncateForErr(body, 200))
|
|
}
|
|
|
|
var wrap struct {
|
|
Status string `json:"status"`
|
|
Data struct {
|
|
Prefixes []struct {
|
|
Prefix string `json:"prefix"`
|
|
} `json:"prefixes"`
|
|
} `json:"data"`
|
|
Messages [][]string `json:"messages"`
|
|
}
|
|
if err := json.Unmarshal(body, &wrap); err != nil {
|
|
return nil, fmt.Errorf("ripestat AS%d: json: %w", asn, err)
|
|
}
|
|
if wrap.Status != "" && wrap.Status != "ok" {
|
|
return nil, fmt.Errorf("ripestat AS%d: status %q", asn, wrap.Status)
|
|
}
|
|
|
|
var out []netip.Prefix
|
|
for _, row := range wrap.Data.Prefixes {
|
|
p := strings.TrimSpace(row.Prefix)
|
|
if p == "" {
|
|
continue
|
|
}
|
|
pfx, err := netip.ParsePrefix(p)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
out = append(out, pfx.Masked())
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].String() < out[j].String() })
|
|
return out, nil
|
|
}
|
|
|
|
// ASHolderName returns the holder / organization label for the ASN from RIPEstat as-overview (best-effort).
|
|
func ASHolderName(ctx context.Context, hc *http.Client, asn int64) (string, error) {
|
|
if hc == nil {
|
|
hc = http.DefaultClient
|
|
}
|
|
base := strings.TrimSpace(os.Getenv("EVOBGP_RIPESTAT_AS_OVERVIEW_URL"))
|
|
if base == "" {
|
|
base = DefaultASOverviewURL
|
|
}
|
|
u := fmt.Sprintf("%s?resource=AS%d", strings.TrimSuffix(base, "?"), asn)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
req.Header.Set("Accept", "application/json")
|
|
req.Header.Set("User-Agent", "evobgp-asnresolve/1.0")
|
|
|
|
resp, err := hc.Do(req)
|
|
if err != nil {
|
|
return "", fmt.Errorf("ripestat as-overview AS%d: %w", asn, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
body, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return "", fmt.Errorf("ripestat as-overview AS%d: HTTP %s: %s", asn, resp.Status, truncateForErr(body, 200))
|
|
}
|
|
|
|
var wrap struct {
|
|
Status string `json:"status"`
|
|
Data struct {
|
|
Holder string `json:"holder"`
|
|
} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(body, &wrap); err != nil {
|
|
return "", fmt.Errorf("ripestat as-overview AS%d: json: %w", asn, err)
|
|
}
|
|
if wrap.Status != "" && wrap.Status != "ok" {
|
|
return "", fmt.Errorf("ripestat as-overview AS%d: status %q", asn, wrap.Status)
|
|
}
|
|
return strings.TrimSpace(wrap.Data.Holder), nil
|
|
}
|
|
|
|
func truncateForErr(b []byte, n int) string {
|
|
s := string(b)
|
|
if len(s) > n {
|
|
return s[:n] + "…"
|
|
}
|
|
return s
|
|
}
|
|
|
|
// PolitePause is a short delay between upstream ASN lookups (same refresh).
|
|
func PolitePause() {
|
|
d := 150 * time.Millisecond
|
|
if s := strings.TrimSpace(os.Getenv("EVOBGP_ASN_RESOLVE_PAUSE_MS")); s != "" {
|
|
if ms, err := parsePositiveInt(s); err == nil && ms > 0 {
|
|
d = time.Duration(ms) * time.Millisecond
|
|
}
|
|
}
|
|
time.Sleep(d)
|
|
}
|
|
|
|
func parsePositiveInt(s string) (int, error) {
|
|
n, err := strconv.Atoi(s)
|
|
if err != nil || n <= 0 {
|
|
return 0, fmt.Errorf("invalid")
|
|
}
|
|
return n, nil
|
|
}
|