Add the public VPN server lists, with is_vpn and vpn_providers (closes #235)
check / check (push) Successful in 1m36s

The IP lookup now checks Mullvad, NordVPN, Private Internet Access, IVPN and X4BNet's community list, each downloaded once a day. Each result has `flags.is_vpn` and `vpn_providers`.

Judgement call: X4BNet publishes IPv4 and IPv6 as two files, so they are two lists, `x4bnet` and `x4bnet_v6`.
Judgement call: Private Internet Access's signature is not read or checked.
Rule suppressed: `tagliatelle` on the structs holding the publishers' field names.

Model: opus-5-5
This commit is contained in:
clawbot committed 2026-11-10 10:56:24 +00:00
1 parent 3204b7ee4b
commit 601bc812b9
8 files changed
+584 -28

No files matched your search

+84 -4
View File
@@ -1441,8 +1441,19 @@ const listedUnroutedAddr = "203.0.113.1"
// torExitAddr is the address that fakeIPLists lists on the Tor exit list.
const torExitAddr = "203.0.113.2"
// fakeIPLists has two lists. The test list holds addrInTestPrefixV4 and
// listedUnroutedAddr, and the Tor exit list holds torExitAddr.
// The addresses that fakeIPLists lists on one and on two VPN lists, and the
// names of those lists.
const (
oneVPNListAddr = "203.0.113.3"
twoVPNListsAddr = "203.0.113.4"
testVPNList = "test_vpn"
otherTestVPN = "other_test_vpn"
)
// fakeIPLists has four lists. The test list holds addrInTestPrefixV4,
// listedUnroutedAddr and twoVPNListsAddr. The Tor exit list holds torExitAddr.
// The test VPN list holds oneVPNListAddr and twoVPNListsAddr, and the other
// test VPN list holds twoVPNListsAddr.
type fakeIPLists struct{}
func testListing() iplist.Listing {
@@ -1463,6 +1474,18 @@ func (fakeIPLists) Lookup(addr netip.Addr) []iplist.Listing {
return []iplist.Listing{{Name: iplist.TorExitsList}}
}
if addr == netip.MustParseAddr(oneVPNListAddr) {
return []iplist.Listing{{Name: testVPNList, VPN: true}}
}
if addr == netip.MustParseAddr(twoVPNListsAddr) {
return []iplist.Listing{
{Name: otherTestVPN, VPN: true},
testListing(),
{Name: testVPNList, VPN: true},
}
}
return []iplist.Listing{}
}
@@ -1471,7 +1494,7 @@ func (fakeIPLists) ShortestInterval() time.Duration {
}
// TestIPLookupFlags looks up addresses with and without a route. Every result
// has both flags, under the JSON keys the API documents.
// has every flag, under the JSON keys the API documents.
func TestIPLookupFlags(t *testing.T) {
t.Parallel()
@@ -1517,7 +1540,9 @@ func TestIPLookupFlags(t *testing.T) {
t.Fatalf("%s: decoding flags %s: %v", path, results[0]["flags"], err)
}
want := map[string]bool{"is_tor": tt.tor, "is_bogon": tt.bogon}
want := map[string]bool{
"is_tor": tt.tor, "is_vpn": false, "is_bogon": tt.bogon,
}
if !maps.Equal(got, want) {
t.Errorf("%s: flags %v, want %v", path, got, want)
}
@@ -1525,6 +1550,61 @@ func TestIPLookupFlags(t *testing.T) {
}
}
// TestIPLookupReportsVPNProviders looks up addresses on no list, on a list
// that is not a VPN list, on one VPN list and on two. is_vpn says whether any
// VPN list holds the address, and vpn_providers names only those lists. An
// address on none has [], not null.
func TestIPLookupReportsVPNProviders(t *testing.T) {
t.Parallel()
s := newServerWithTestRoutes(t)
s.resolver = &fakeResolver{}
s.SetIPLists(fakeIPLists{})
tests := []struct {
name string
addr string
isVPN string
providers string
}{
{"on no list", "1.1.1.1", "false", `[]`},
{"on a list that is not a VPN list", listedUnroutedAddr, "false", `[]`},
{"on one VPN list", oneVPNListAddr, "true", `["` + testVPNList + `"]`},
{
"on two VPN lists", twoVPNListsAddr, "true",
`["` + otherTestVPN + `","` + testVPNList + `"]`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
path := "/api/v1/ip/" + tt.addr
results := resultFields(t, get(t, s, path).Body.Bytes())
if len(results) != 1 {
t.Fatalf("%s: %d results, want 1", path, len(results))
}
var flags map[string]json.RawMessage
err := json.Unmarshal(results[0]["flags"], &flags)
if err != nil {
t.Fatalf("%s: decoding flags %s: %v", path, results[0]["flags"], err)
}
if got := string(flags["is_vpn"]); got != tt.isVPN {
t.Errorf("%s: is_vpn %s, want %s", path, got, tt.isVPN)
}
if got := string(results[0]["vpn_providers"]); got != tt.providers {
t.Errorf("%s: vpn_providers %s, want %s", path, got, tt.providers)
}
})
}
}
// TestIPLookupReportsIPLists looks up two routed addresses, one of them on an
// IP list. The listed address's result names the list, says what it means and
// when it was updated. The other's result has an empty list.