184 lines
6.1 KiB
Go
184 lines
6.1 KiB
Go
|
|
package main
|
||
|
|
|
||
|
|
import (
|
||
|
|
"archive/zip"
|
||
|
|
"fmt"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
func writeZipFiles(t *testing.T, files map[string]string) string {
|
||
|
|
t.Helper()
|
||
|
|
path := filepath.Join(t.TempDir(), "fixture.zip")
|
||
|
|
output, err := os.Create(path)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
archive := zip.NewWriter(output)
|
||
|
|
for name, contents := range files {
|
||
|
|
entry, err := archive.Create(name)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if _, err := fmt.Fprint(entry, contents); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if err := archive.Close(); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if err := output.Close(); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
return path
|
||
|
|
}
|
||
|
|
|
||
|
|
func mustAddress(t *testing.T, value string) uint32 {
|
||
|
|
t.Helper()
|
||
|
|
address, ok := rangeFromPrefix(value + "/32")
|
||
|
|
if !ok {
|
||
|
|
t.Fatalf("invalid test address %q", value)
|
||
|
|
}
|
||
|
|
return address.start
|
||
|
|
}
|
||
|
|
|
||
|
|
func rangeContains(ranges []addressRange, address uint32) bool {
|
||
|
|
for _, candidate := range ranges {
|
||
|
|
if candidate.start <= address && candidate.end >= address {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
func rangesEqual(left, right []addressRange) bool {
|
||
|
|
left = mergeRanges(left)
|
||
|
|
right = mergeRanges(right)
|
||
|
|
if len(left) != len(right) {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
for index := range left {
|
||
|
|
if left[index] != right[index] {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestGeographySetsSeparateVPNExceptionCountries(t *testing.T) {
|
||
|
|
path := writeZipFiles(t, map[string]string{
|
||
|
|
"ip-to-country.csv": "network,country_code,continent_code\n192.0.2.0/28,JP,AS\n192.0.2.16/28,KR,AS\n198.51.100.0/24,US,NA\n203.0.113.0/24,BR,SA\n",
|
||
|
|
})
|
||
|
|
sets, err := ipLocateGeography(path)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if !rangeContains(sets.admission, mustAddress(t, "198.51.100.1")) || rangeContains(sets.admission, mustAddress(t, "203.0.113.1")) {
|
||
|
|
t.Fatalf("unexpected admission ranges: %#v", sets.admission)
|
||
|
|
}
|
||
|
|
if !rangeContains(sets.vpnException, mustAddress(t, "192.0.2.1")) || !rangeContains(sets.vpnException, mustAddress(t, "192.0.2.17")) || rangeContains(sets.vpnException, mustAddress(t, "198.51.100.1")) {
|
||
|
|
t.Fatalf("unexpected VPN exception ranges: %#v", sets.vpnException)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMaxMindGeographySetsSeparateVPNExceptionCountries(t *testing.T) {
|
||
|
|
path := writeZipFiles(t, map[string]string{
|
||
|
|
"GeoLite2-Country-Locations-en.csv": "geoname_id,locale_code,continent_code,country_iso_code\n1,en,AS,JP\n2,en,AS,KR\n3,en,NA,US\n4,en,SA,BR\n",
|
||
|
|
"GeoLite2-Country-Blocks-IPv4.csv": "network,geoname_id,registered_country_geoname_id\n192.0.2.0/28,1,1\n192.0.2.16/28,2,2\n198.51.100.0/24,3,3\n203.0.113.0/24,4,4\n",
|
||
|
|
})
|
||
|
|
sets, err := maxMindGeography(path)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if !rangeContains(sets.admission, mustAddress(t, "198.51.100.1")) || rangeContains(sets.admission, mustAddress(t, "203.0.113.1")) {
|
||
|
|
t.Fatalf("unexpected admission ranges: %#v", sets.admission)
|
||
|
|
}
|
||
|
|
if !rangeContains(sets.vpnException, mustAddress(t, "192.0.2.1")) || !rangeContains(sets.vpnException, mustAddress(t, "192.0.2.17")) || rangeContains(sets.vpnException, mustAddress(t, "198.51.100.1")) {
|
||
|
|
t.Fatalf("unexpected VPN exception ranges: %#v", sets.vpnException)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestIPLocateASNRangesSelectConfiguredOperators(t *testing.T) {
|
||
|
|
path := writeZipFiles(t, map[string]string{
|
||
|
|
"ip-to-asn.csv": "network,asn,country_code,name,org,domain\n192.0.2.0/28,147049,AU,selected,selected,example.test\n192.0.2.16/28,64500,JP,other,other,example.test\n198.51.100.0/24,AS147049,AU,selected,selected,example.test\n",
|
||
|
|
})
|
||
|
|
ranges, err := ipLocateASNRanges(path, map[uint32]struct{}{147049: {}})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if !rangeContains(ranges, mustAddress(t, "192.0.2.1")) || !rangeContains(ranges, mustAddress(t, "198.51.100.1")) || rangeContains(ranges, mustAddress(t, "192.0.2.17")) {
|
||
|
|
t.Fatalf("unexpected ASN ranges: %#v", ranges)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestVPNExceptionCompositionPreservesConsensusAndDenies(t *testing.T) {
|
||
|
|
consensus := []addressRange{{start: 10, end: 20}}
|
||
|
|
vpnASN := []addressRange{{start: 15, end: 30}}
|
||
|
|
vpnGeography := []addressRange{{start: 18, end: 28}}
|
||
|
|
denied := []addressRange{{start: 24, end: 25}}
|
||
|
|
candidate := subtractRanges(intersectRanges(vpnASN, vpnGeography), consensus)
|
||
|
|
exceptions := subtractRanges(candidate, denied)
|
||
|
|
want := []addressRange{{start: 21, end: 23}, {start: 26, end: 28}}
|
||
|
|
if !rangesEqual(exceptions, want) {
|
||
|
|
t.Fatalf("exceptions=%#v want=%#v", exceptions, want)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestParseASN(t *testing.T) {
|
||
|
|
for _, value := range []string{"147049", "AS147049", "as147049"} {
|
||
|
|
asn, err := parseASN(value)
|
||
|
|
if err != nil || asn != 147049 {
|
||
|
|
t.Fatalf("parseASN(%q)=%d,%v", value, asn, err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for _, value := range []string{"", "AS", "0", "-1", "4294967296"} {
|
||
|
|
if _, err := parseASN(value); err == nil {
|
||
|
|
t.Fatalf("parseASN(%q) succeeded", value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestLoadIP2ProxyClassificationsOmitsStaleCache(t *testing.T) {
|
||
|
|
now := time.Unix(2000000000, 0)
|
||
|
|
path := filepath.Join(t.TempDir(), "ip2proxy.zip")
|
||
|
|
if err := os.WriteFile(path, []byte("invalid archive"), 0o600); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
modified := now.Add(-36 * 24 * time.Hour)
|
||
|
|
if err := os.Chtimes(path, modified, modified); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
result, state, age, err := loadIP2ProxyClassifications(path, now, 35*24*time.Hour)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if state != "stale" || age != 36*24*time.Hour || len(result.all) != 0 {
|
||
|
|
t.Fatalf("state=%s age=%s ranges=%d", state, age, len(result.all))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestLoadIP2ProxyClassificationsRequiresValidFreshCache(t *testing.T) {
|
||
|
|
now := time.Unix(2000000000, 0)
|
||
|
|
path := filepath.Join(t.TempDir(), "ip2proxy.zip")
|
||
|
|
if err := os.WriteFile(path, []byte("invalid archive"), 0o600); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if err := os.Chtimes(path, now, now); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if _, _, _, err := loadIP2ProxyClassifications(path, now, 35*24*time.Hour); err == nil {
|
||
|
|
t.Fatal("expected fresh invalid cache to fail")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestLoadIP2ProxyClassificationsAllowsAbsentCache(t *testing.T) {
|
||
|
|
result, state, age, err := loadIP2ProxyClassifications(filepath.Join(t.TempDir(), "missing.zip"), time.Now(), 35*24*time.Hour)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if state != "absent" || age != 0 || len(result.all) != 0 {
|
||
|
|
t.Fatalf("state=%s age=%s ranges=%d", state, age, len(result.all))
|
||
|
|
}
|
||
|
|
}
|