quorate/packages/oci-l4-relay-publisher/main.go

900 lines
25 KiB
Go

package main
import (
"archive/zip"
"bufio"
"compress/gzip"
"encoding/csv"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
)
func headerIndexes(header []string) map[string]int {
result := make(map[string]int, len(header))
for index, name := range header {
result[name] = index
}
return result
}
func fileWithSuffix(archive *zip.ReadCloser, suffix string) (*zip.File, error) {
for _, file := range archive.File {
if strings.HasSuffix(file.Name, suffix) {
return file, nil
}
}
return nil, fmt.Errorf("missing %s", suffix)
}
func openCSV(file *zip.File) (*csv.Reader, io.Closer, error) {
contents, err := file.Open()
if err != nil {
return nil, nil, err
}
return csv.NewReader(contents), contents, nil
}
func selectedCountry(country, continent string) bool {
return country == "JP" || country == "KR" || country == "SG" || country == "US" || continent == "EU"
}
func vpnExceptionCountry(country string) bool {
return country == "JP" || country == "KR"
}
type geographySets struct {
admission []addressRange
vpnException []addressRange
}
type countrySelection struct {
admission bool
vpnException bool
}
func maxMindGeography(path string) (geographySets, error) {
archive, err := zip.OpenReader(path)
if err != nil {
return geographySets{}, err
}
defer func() { _ = archive.Close() }()
locationFile, err := fileWithSuffix(archive, "Country-Locations-en.csv")
if err != nil {
return geographySets{}, err
}
locationReader, locationContents, err := openCSV(locationFile)
if err != nil {
return geographySets{}, err
}
locationHeader, err := locationReader.Read()
if err != nil {
_ = locationContents.Close()
return geographySets{}, err
}
locationFields := headerIndexes(locationHeader)
locations := make(map[int]countrySelection)
for {
row, err := locationReader.Read()
if err == io.EOF {
break
}
if err != nil {
_ = locationContents.Close()
return geographySets{}, err
}
id, err := strconv.Atoi(row[locationFields["geoname_id"]])
if err == nil {
country := row[locationFields["country_iso_code"]]
locations[id] = countrySelection{
admission: selectedCountry(country, row[locationFields["continent_code"]]),
vpnException: vpnExceptionCountry(country),
}
}
}
if err := locationContents.Close(); err != nil {
return geographySets{}, err
}
blocksFile, err := fileWithSuffix(archive, "Country-Blocks-IPv4.csv")
if err != nil {
return geographySets{}, err
}
blocksReader, blocksContents, err := openCSV(blocksFile)
if err != nil {
return geographySets{}, err
}
defer func() { _ = blocksContents.Close() }()
blocksHeader, err := blocksReader.Read()
if err != nil {
return geographySets{}, err
}
blockFields := headerIndexes(blocksHeader)
result := geographySets{}
for {
row, err := blocksReader.Read()
if err == io.EOF {
break
}
if err != nil {
return geographySets{}, err
}
idText := row[blockFields["geoname_id"]]
if idText == "" {
idText = row[blockFields["registered_country_geoname_id"]]
}
id, err := strconv.Atoi(idText)
selection, found := locations[id]
if err != nil || !found {
continue
}
if network, ok := rangeFromPrefix(row[blockFields["network"]]); ok {
if selection.admission {
result.admission = append(result.admission, network)
}
if selection.vpnException {
result.vpnException = append(result.vpnException, network)
}
}
}
result.admission = mergeRanges(result.admission)
result.vpnException = mergeRanges(result.vpnException)
return result, nil
}
func ipLocateGeography(path string) (geographySets, error) {
archive, err := zip.OpenReader(path)
if err != nil {
return geographySets{}, err
}
defer func() { _ = archive.Close() }()
file, err := fileWithSuffix(archive, ".csv")
if err != nil {
return geographySets{}, err
}
reader, contents, err := openCSV(file)
if err != nil {
return geographySets{}, err
}
defer func() { _ = contents.Close() }()
header, err := reader.Read()
if err != nil {
return geographySets{}, err
}
fields := headerIndexes(header)
result := geographySets{}
for {
row, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
return geographySets{}, err
}
country := row[fields["country_code"]]
if network, ok := rangeFromPrefix(row[fields["network"]]); ok {
if selectedCountry(country, row[fields["continent_code"]]) {
result.admission = append(result.admission, network)
}
if vpnExceptionCountry(country) {
result.vpnException = append(result.vpnException, network)
}
}
}
result.admission = mergeRanges(result.admission)
result.vpnException = mergeRanges(result.vpnException)
return result, nil
}
func parseASN(value string) (uint32, error) {
value = strings.TrimSpace(value)
if len(value) > 2 && strings.EqualFold(value[:2], "AS") {
value = value[2:]
}
parsed, err := strconv.ParseUint(value, 10, 32)
if err != nil || parsed == 0 {
return 0, fmt.Errorf("invalid ASN %q", value)
}
return uint32(parsed), nil
}
func ipLocateASNRanges(path string, selected map[uint32]struct{}) ([]addressRange, error) {
if len(selected) == 0 {
return nil, nil
}
archive, err := zip.OpenReader(path)
if err != nil {
return nil, err
}
defer func() { _ = archive.Close() }()
file, err := fileWithSuffix(archive, ".csv")
if err != nil {
return nil, err
}
reader, contents, err := openCSV(file)
if err != nil {
return nil, err
}
defer func() { _ = contents.Close() }()
header, err := reader.Read()
if err != nil {
return nil, err
}
fields := headerIndexes(header)
var result []addressRange
for {
row, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
asn, err := parseASN(row[fields["asn"]])
if err != nil {
continue
}
if _, found := selected[asn]; !found {
continue
}
if network, ok := rangeFromPrefix(row[fields["network"]]); ok {
result = append(result, network)
}
}
return mergeRanges(result), nil
}
func jsonContents(path string, target any) error {
contents, err := os.ReadFile(path)
if err != nil {
return err
}
return json.Unmarshal(contents, target)
}
func prefixRanges(prefixes []string) []addressRange {
result := make([]addressRange, 0, len(prefixes))
for _, prefix := range prefixes {
if network, ok := rangeFromPrefix(prefix); ok {
result = append(result, network)
}
}
return mergeRanges(result)
}
func awsRanges(path string) ([]addressRange, error) {
var document struct {
Prefixes []struct {
Prefix string `json:"ip_prefix"`
} `json:"prefixes"`
}
if err := jsonContents(path, &document); err != nil {
return nil, err
}
prefixes := make([]string, 0, len(document.Prefixes))
for _, item := range document.Prefixes {
prefixes = append(prefixes, item.Prefix)
}
return prefixRanges(prefixes), nil
}
func gcpRanges(path string) ([]addressRange, error) {
var document struct {
Prefixes []struct {
Prefix string `json:"ipv4Prefix"`
} `json:"prefixes"`
}
if err := jsonContents(path, &document); err != nil {
return nil, err
}
prefixes := make([]string, 0, len(document.Prefixes))
for _, item := range document.Prefixes {
prefixes = append(prefixes, item.Prefix)
}
return prefixRanges(prefixes), nil
}
func azureRanges(path string) ([]addressRange, error) {
var document struct {
Values []struct {
Properties struct {
Prefixes []string `json:"addressPrefixes"`
} `json:"properties"`
} `json:"values"`
}
if err := jsonContents(path, &document); err != nil {
return nil, err
}
var prefixes []string
for _, value := range document.Values {
prefixes = append(prefixes, value.Properties.Prefixes...)
}
return prefixRanges(prefixes), nil
}
func ociRanges(path string) ([]addressRange, error) {
var document struct {
Regions []struct {
CIDRs []struct {
CIDR string `json:"cidr"`
} `json:"cidrs"`
} `json:"regions"`
}
if err := jsonContents(path, &document); err != nil {
return nil, err
}
var prefixes []string
for _, region := range document.Regions {
for _, cidr := range region.CIDRs {
prefixes = append(prefixes, cidr.CIDR)
}
}
return prefixRanges(prefixes), nil
}
func fastlyRanges(path string) ([]addressRange, error) {
var document struct {
Addresses []string `json:"addresses"`
}
if err := jsonContents(path, &document); err != nil {
return nil, err
}
return prefixRanges(document.Addresses), nil
}
func githubRanges(path string) ([]addressRange, error) {
var document map[string]json.RawMessage
if err := jsonContents(path, &document); err != nil {
return nil, err
}
var prefixes []string
for _, contents := range document {
var values []string
if err := json.Unmarshal(contents, &values); err == nil {
prefixes = append(prefixes, values...)
}
}
return prefixRanges(prefixes), nil
}
func atlassianRanges(path string) ([]addressRange, error) {
var document struct {
Items []struct {
CIDR string `json:"cidr"`
} `json:"items"`
}
if err := jsonContents(path, &document); err != nil {
return nil, err
}
prefixes := make([]string, 0, len(document.Items))
for _, item := range document.Items {
prefixes = append(prefixes, item.CIDR)
}
return prefixRanges(prefixes), nil
}
func geofeedRanges(path string) ([]addressRange, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer func() { _ = file.Close() }()
reader := csv.NewReader(file)
reader.Comment = '#'
reader.FieldsPerRecord = -1
var result []addressRange
for {
row, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
if len(row) == 0 {
continue
}
if network, ok := rangeFromPrefix(strings.TrimSpace(row[0])); ok {
result = append(result, network)
}
}
return mergeRanges(result), nil
}
func lineRanges(path string) ([]addressRange, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer func() { _ = file.Close() }()
var result []addressRange
scanner := bufio.NewScanner(file)
for scanner.Scan() {
value := strings.TrimSpace(scanner.Text())
if value == "" {
continue
}
if !strings.Contains(value, "/") {
value += "/32"
}
if network, ok := rangeFromPrefix(value); ok {
result = append(result, network)
}
}
return mergeRanges(result), scanner.Err()
}
func spamhausRanges(path string) ([]addressRange, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer func() { _ = file.Close() }()
decoder := json.NewDecoder(file)
var result []addressRange
for {
var record struct {
CIDR string `json:"cidr"`
}
if err := decoder.Decode(&record); err == io.EOF {
break
} else if err != nil {
return nil, err
}
if network, ok := rangeFromPrefix(record.CIDR); ok {
result = append(result, network)
}
}
return mergeRanges(result), nil
}
func unionRanges(sets ...[]addressRange) []addressRange {
var result []addressRange
for _, set := range sets {
result = append(result, set...)
}
return mergeRanges(result)
}
func percent(part, whole uint64) float64 {
if whole == 0 {
return 0
}
return float64(part) * 100 / float64(whole)
}
type categoryStat struct {
records int
addresses uint64
}
type ip2ProxyStats struct {
all []addressRange
recentSevenDays []addressRange
threat []addressRange
highFraudScore []addressRange
usageTypes map[string]categoryStat
proxyTypes map[string]categoryStat
threatRecords int
positiveFraudScores int
minimumLastSeen int
maximumLastSeen int
}
func incrementCategory(categories map[string]categoryStat, name string, size uint64) {
current := categories[name]
current.records++
current.addresses += size
categories[name] = current
}
func ip2ProxyClassifications(path string) (ip2ProxyStats, error) {
archive, err := zip.OpenReader(path)
if err != nil {
return ip2ProxyStats{}, err
}
defer func() { _ = archive.Close() }()
file, err := fileWithSuffix(archive, "IP2PROXY-LITE-PX12.CSV")
if err != nil {
return ip2ProxyStats{}, err
}
reader, contents, err := openCSV(file)
if err != nil {
return ip2ProxyStats{}, err
}
defer func() { _ = contents.Close() }()
result := ip2ProxyStats{usageTypes: make(map[string]categoryStat), proxyTypes: make(map[string]categoryStat)}
for {
row, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
return ip2ProxyStats{}, err
}
if len(row) != 16 {
return ip2ProxyStats{}, fmt.Errorf("unexpected IP2Proxy field count %d", len(row))
}
start, err := strconv.ParseUint(row[0], 10, 32)
if err != nil {
return ip2ProxyStats{}, err
}
end, err := strconv.ParseUint(row[1], 10, 32)
if err != nil {
return ip2ProxyStats{}, err
}
network := addressRange{start: uint32(start), end: uint32(end)}
size := end - start + 1
proxyType := row[2]
usageType := row[9]
incrementCategory(result.proxyTypes, proxyType, size)
incrementCategory(result.usageTypes, usageType, size)
result.all = append(result.all, network)
if row[13] != "-" {
result.threatRecords++
result.threat = append(result.threat, network)
}
lastSeen, err := strconv.Atoi(row[12])
if err == nil && lastSeen > 0 {
if result.minimumLastSeen == 0 || lastSeen < result.minimumLastSeen {
result.minimumLastSeen = lastSeen
}
if lastSeen > result.maximumLastSeen {
result.maximumLastSeen = lastSeen
}
if lastSeen <= 7 {
result.recentSevenDays = append(result.recentSevenDays, network)
}
}
fraudScore, err := strconv.Atoi(row[15])
if err == nil && fraudScore > 0 {
result.positiveFraudScores++
}
if err == nil && fraudScore >= 80 {
result.highFraudScore = append(result.highFraudScore, network)
}
}
result.all = mergeRanges(result.all)
result.recentSevenDays = mergeRanges(result.recentSevenDays)
result.threat = mergeRanges(result.threat)
result.highFraudScore = mergeRanges(result.highFraudScore)
return result, nil
}
func loadIP2ProxyClassifications(path string, now time.Time, maximumAge time.Duration) (ip2ProxyStats, string, time.Duration, error) {
empty := ip2ProxyStats{usageTypes: make(map[string]categoryStat), proxyTypes: make(map[string]categoryStat)}
if maximumAge <= 0 {
return empty, "", 0, errors.New("maximum IP2Proxy age must be positive")
}
info, err := os.Stat(path)
if errors.Is(err, os.ErrNotExist) {
return empty, "absent", 0, nil
}
if err != nil {
return empty, "", 0, err
}
if info.ModTime().After(now.Add(5 * time.Minute)) {
return empty, "", 0, errors.New("IP2Proxy cache modification time is in the future")
}
age := now.Sub(info.ModTime())
if age > maximumAge {
return empty, "stale", age, nil
}
classifications, err := ip2ProxyClassifications(path)
if err != nil {
return empty, "", age, err
}
return classifications, "fresh", age, nil
}
func printCategories(name string, categories map[string]categoryStat) {
keys := make([]string, 0, len(categories))
for key := range categories {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
label := key
if label == "-" {
label = "unclassified"
}
fmt.Printf("%s_%s_records=%d\n", name, strings.ToLower(label), categories[key].records)
fmt.Printf("%s_%s_addresses=%d\n", name, strings.ToLower(label), categories[key].addresses)
}
}
func printSet(name string, ranges []addressRange) {
fmt.Printf("%s_intervals=%d\n", name, len(ranges))
fmt.Printf("%s_addresses=%d\n", name, addressCount(ranges))
}
func ipv4String(value uint32) string {
return fmt.Sprintf("%d.%d.%d.%d", byte(value>>24), byte(value>>16), byte(value>>8), byte(value))
}
func writeNftSet(writer *bufio.Writer, name string, ranges []addressRange) error {
if _, err := fmt.Fprintf(writer, " set %s {\n type ipv4_addr\n flags interval\n auto-merge\n elements = {\n", name); err != nil {
return err
}
for index, current := range ranges {
separator := ","
if index == len(ranges)-1 {
separator = ""
}
value := ipv4String(current.start)
if current.start != current.end {
value += "-" + ipv4String(current.end)
}
if _, err := fmt.Fprintf(writer, " %s%s\n", value, separator); err != nil {
return err
}
}
_, err := fmt.Fprint(writer, " }\n }\n")
return err
}
func writeNft(path string, geography, denied []addressRange) error {
file, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600)
if err != nil {
return err
}
writer := bufio.NewWriter(file)
if _, err := fmt.Fprintln(writer, "table inet relay_spike {"); err != nil {
_ = file.Close()
return err
}
if err := writeNftSet(writer, "geography_v4", geography); err != nil {
_ = file.Close()
return err
}
if err := writeNftSet(writer, "deny_v4", denied); err != nil {
_ = file.Close()
return err
}
if _, err := fmt.Fprintln(writer, "}"); err != nil {
_ = file.Close()
return err
}
if err := writer.Flush(); err != nil {
_ = file.Close()
return err
}
return file.Close()
}
type bundleRange struct {
Start string `json:"start"`
End string `json:"end"`
}
type admissionBundle struct {
SchemaVersion int `json:"schema_version"`
GeneratedAt time.Time `json:"generated_at"`
ExpiresAt time.Time `json:"expires_at"`
AdmittedIPv4 []bundleRange `json:"admitted_ipv4"`
ExceptionIPv4 []bundleRange `json:"exception_ipv4"`
AdmittedIntervalCount int `json:"admitted_interval_count"`
AdmittedAddressCount uint64 `json:"admitted_address_count"`
ExceptionIntervalCount int `json:"exception_interval_count"`
ExceptionAddressCount uint64 `json:"exception_address_count"`
}
func writeBundle(path string, admitted, exceptions []addressRange, now time.Time) error {
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
return err
}
file, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600)
if err != nil {
return err
}
compressed := gzip.NewWriter(file)
document := admissionBundle{
SchemaVersion: 1,
GeneratedAt: now.UTC(),
ExpiresAt: now.UTC().Add(72 * time.Hour),
AdmittedIPv4: make([]bundleRange, 0, len(admitted)),
ExceptionIPv4: make([]bundleRange, 0, len(exceptions)),
AdmittedIntervalCount: len(admitted),
AdmittedAddressCount: addressCount(admitted),
ExceptionIntervalCount: len(exceptions),
ExceptionAddressCount: addressCount(exceptions),
}
for _, item := range admitted {
document.AdmittedIPv4 = append(document.AdmittedIPv4, bundleRange{Start: ipv4String(item.start), End: ipv4String(item.end)})
}
for _, item := range exceptions {
document.ExceptionIPv4 = append(document.ExceptionIPv4, bundleRange{Start: ipv4String(item.start), End: ipv4String(item.end)})
}
encodeErr := json.NewEncoder(compressed).Encode(document)
closeGzipErr := compressed.Close()
closeFileErr := file.Close()
if encodeErr != nil {
return encodeErr
}
if closeGzipErr != nil {
return closeGzipErr
}
return closeFileErr
}
func main() {
nftOutput := flag.String("nft-output", "", "write selective nftables sets")
nftFullOutput := flag.String("nft-full-output", "", "write complete nftables sets")
bundleOutput := flag.String("bundle-output", "", "write normalized selective admission bundle")
maximumIP2ProxyAge := flag.Duration("maximum-ip2proxy-age", 35*24*time.Hour, "omit IP2Proxy classifications after this cache age")
vpnExceptionASNs := map[uint32]struct{}{}
flag.Func("vpn-exception-asn", "VPN operator ASN admitted only within MaxMind or IPLocate JP/KR ranges", func(value string) error {
asn, err := parseASN(value)
if err != nil {
return err
}
vpnExceptionASNs[asn] = struct{}{}
return nil
})
flag.Parse()
now := time.Now().UTC()
maxMind, err := maxMindGeography("data/maxmind-country.zip")
if err != nil {
panic(err)
}
ipLocate, err := ipLocateGeography("data/iplocate-country.zip")
if err != nil {
panic(err)
}
vpnASN, err := ipLocateASNRanges("data/iplocate-asn.zip", vpnExceptionASNs)
if err != nil {
panic(err)
}
aws, err := awsRanges("data/aws.json")
if err != nil {
panic(err)
}
gcp, err := gcpRanges("data/gcp.json")
if err != nil {
panic(err)
}
azure, err := azureRanges("data/azure.json")
if err != nil {
panic(err)
}
oci, err := ociRanges("data/oci.json")
if err != nil {
panic(err)
}
cloudflare, err := lineRanges("data/cloudflare.txt")
if err != nil {
panic(err)
}
tor, err := lineRanges("data/tor-exits.txt")
if err != nil {
panic(err)
}
spamhaus, err := spamhausRanges("data/spamhaus-drop.json")
if err != nil {
panic(err)
}
fastly, err := fastlyRanges("data/fastly.json")
if err != nil {
panic(err)
}
github, err := githubRanges("data/github.json")
if err != nil {
panic(err)
}
atlassian, err := atlassianRanges("data/atlassian.json")
if err != nil {
panic(err)
}
digitalOcean, err := geofeedRanges("data/digitalocean.csv")
if err != nil {
panic(err)
}
linode, err := geofeedRanges("data/linode.csv")
if err != nil {
panic(err)
}
vultr, err := geofeedRanges("data/vultr.csv")
if err != nil {
panic(err)
}
ip2Proxy, ip2ProxyState, ip2ProxyAge, err := loadIP2ProxyClassifications("data/ip2proxy.zip", now, *maximumIP2ProxyAge)
if err != nil {
panic(err)
}
fmt.Printf("ip2proxy_cache_state=%s\n", ip2ProxyState)
if ip2ProxyState != "absent" {
fmt.Printf("ip2proxy_cache_age_hours=%.2f\n", ip2ProxyAge.Hours())
}
fmt.Printf("ip2proxy_cache_omitted=%t\n", ip2ProxyState != "fresh")
consensus := intersectRanges(maxMind.admission, ipLocate.admission)
geoUnion := unionRanges(maxMind.admission, ipLocate.admission)
geoDisagreement := addressCount(geoUnion) - addressCount(consensus)
vpnExceptionGeography := unionRanges(maxMind.vpnException, ipLocate.vpnException)
vpnExceptionCandidate := subtractRanges(intersectRanges(vpnASN, vpnExceptionGeography), consensus)
cloud := unionRanges(aws, gcp, azure, oci, cloudflare, fastly, github, atlassian, digitalOcean, linode, vultr)
denied := unionRanges(cloud, tor, spamhaus, ip2Proxy.all)
selectiveProxy := unionRanges(ip2Proxy.recentSevenDays, ip2Proxy.threat)
selectiveDenied := unionRanges(cloud, tor, spamhaus, selectiveProxy)
deniedInConsensus := intersectRanges(consensus, denied)
selectiveDeniedInConsensus := intersectRanges(consensus, selectiveDenied)
candidate := subtractRanges(consensus, denied)
selectiveCandidate := subtractRanges(consensus, selectiveDenied)
vpnExceptions := subtractRanges(vpnExceptionCandidate, denied)
selectiveVPNExceptions := subtractRanges(vpnExceptionCandidate, selectiveDenied)
printSet("maxmind_geo", maxMind.admission)
printSet("iplocate_geo", ipLocate.admission)
printSet("geo_consensus", consensus)
fmt.Printf("geo_disagreement_addresses=%d\n", geoDisagreement)
fmt.Printf("geo_disagreement_percent_of_union=%.6f\n", percent(geoDisagreement, addressCount(geoUnion)))
printSet("vpn_exception_geo_union", vpnExceptionGeography)
printSet("vpn_exception_asn", vpnASN)
printSet("vpn_exception_candidate_before_denies", vpnExceptionCandidate)
printSet("vpn_exception_after_denies", vpnExceptions)
printSet("selective_vpn_exception_after_denies", selectiveVPNExceptions)
printSet("aws", aws)
printSet("gcp", gcp)
printSet("azure", azure)
printSet("oci", oci)
printSet("cloudflare", cloudflare)
printSet("fastly", fastly)
printSet("github", github)
printSet("atlassian", atlassian)
printSet("digitalocean", digitalOcean)
printSet("linode", linode)
printSet("vultr", vultr)
printSet("cloud_union", cloud)
printSet("tor", tor)
printSet("spamhaus", spamhaus)
printSet("deny_union", denied)
printSet("deny_in_geo_consensus", deniedInConsensus)
fmt.Printf("deny_percent_of_geo_consensus=%.6f\n", percent(addressCount(deniedInConsensus), addressCount(consensus)))
printSet("candidate_after_denies", candidate)
printSet("selective_proxy", selectiveProxy)
printSet("selective_deny_union", selectiveDenied)
printSet("selective_deny_in_geo_consensus", selectiveDeniedInConsensus)
fmt.Printf("selective_deny_percent_of_geo_consensus=%.6f\n", percent(addressCount(selectiveDeniedInConsensus), addressCount(consensus)))
printSet("selective_candidate_after_denies", selectiveCandidate)
printSet("ip2proxy_open_proxy", ip2Proxy.all)
printSet("ip2proxy_recent_seven_days", ip2Proxy.recentSevenDays)
printSet("ip2proxy_threat", ip2Proxy.threat)
printSet("ip2proxy_high_fraud_score", ip2Proxy.highFraudScore)
printCategories("ip2proxy_usage", ip2Proxy.usageTypes)
printCategories("ip2proxy_proxy", ip2Proxy.proxyTypes)
fmt.Printf("ip2proxy_threat_records=%d\n", ip2Proxy.threatRecords)
fmt.Printf("ip2proxy_positive_fraud_score_records=%d\n", ip2Proxy.positiveFraudScores)
fmt.Printf("ip2proxy_minimum_last_seen_days=%d\n", ip2Proxy.minimumLastSeen)
fmt.Printf("ip2proxy_maximum_last_seen_days=%d\n", ip2Proxy.maximumLastSeen)
fmt.Println("open_proxy_classification=true")
fmt.Println("consumer_classification=false")
fmt.Println("generic_vpn_classification=false")
fmt.Println("residential_proxy_classification=false")
fmt.Printf("explicit_vpn_exception_asns=%d\n", len(vpnExceptionASNs))
if *nftOutput != "" {
if err := writeNft(*nftOutput, unionRanges(consensus, vpnExceptionCandidate), selectiveDenied); err != nil {
panic(err)
}
fmt.Println("nft_output_written=true")
}
if *nftFullOutput != "" {
if err := writeNft(*nftFullOutput, unionRanges(consensus, vpnExceptionCandidate), denied); err != nil {
panic(err)
}
fmt.Println("nft_full_output_written=true")
}
if *bundleOutput != "" {
if err := writeBundle(*bundleOutput, selectiveCandidate, selectiveVPNExceptions, now); err != nil {
panic(err)
}
fmt.Println("bundle_output_written=true")
}
}