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
215 lines
5.7 KiB
Go
215 lines
5.7 KiB
Go
// Package iplist downloads public IP lists on a schedule, keeps the last good
|
|
// copy of each on disk and in memory, and answers which lists contain an
|
|
// address.
|
|
package iplist
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"net/netip"
|
|
"sync"
|
|
"time"
|
|
|
|
"go4.org/netipx"
|
|
)
|
|
|
|
// Format is how a list's file is written.
|
|
type Format int
|
|
|
|
const (
|
|
// FormatLines has one address or network per line, such as 192.0.2.1 or
|
|
// 192.0.2.0/24. Lines that start with # are comments.
|
|
FormatLines Format = iota
|
|
|
|
// FormatSpamhausJSON has one JSON object per line, with a network in its
|
|
// cidr field. The last line holds the file's metadata and no network.
|
|
FormatSpamhausJSON
|
|
|
|
// FormatDShield has one network per line, as tab-separated fields that
|
|
// start with the network's first and last address. Lines that start
|
|
// with # are comments.
|
|
FormatDShield
|
|
|
|
// FormatOnionooJSON is one Onionoo details document. Its relays have
|
|
// addresses in two fields: exit_addresses, which has IPv4 only, and
|
|
// or_addresses, which has IPv4 and IPv6, each with a port.
|
|
FormatOnionooJSON
|
|
|
|
// FormatMullvadJSON is Mullvad's JSON list of relays. A relay's addresses
|
|
// are in ipv4_addr_in, ipv6_addr_in and ipv4_v2ray.
|
|
FormatMullvadJSON
|
|
|
|
// FormatNordVPNJSON is NordVPN's JSON list of servers. A server's
|
|
// addresses are in station and ipv6_station.
|
|
FormatNordVPNJSON
|
|
|
|
// FormatPIAJSON is Private Internet Access's JSON server list, then a
|
|
// blank line and a signature. Each region's servers has lists of servers
|
|
// by protocol, and each server has its address in ip.
|
|
FormatPIAJSON
|
|
|
|
// FormatIVPNJSON is IVPN's JSON server list. The hosts of its wireguard
|
|
// and openvpn gateways have their addresses in host and v2ray.
|
|
FormatIVPNJSON
|
|
)
|
|
|
|
// List is one public IP list.
|
|
type List struct {
|
|
// Name names the list in lookup answers, and names its copy on disk.
|
|
Name string
|
|
|
|
// Description says in one line what being on the list means.
|
|
Description string
|
|
|
|
// URL is where the publisher serves the whole list.
|
|
URL string
|
|
|
|
// Interval is how long a copy is used before the list is downloaded
|
|
// again.
|
|
Interval time.Duration
|
|
|
|
// Format is how the list's file is written.
|
|
Format Format
|
|
|
|
// VPN says that the list holds servers of VPN services.
|
|
VPN bool
|
|
}
|
|
|
|
// Listing says that a list contains an address.
|
|
type Listing struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
|
|
// Updated is when routewatch last downloaded the list, or last heard from
|
|
// its publisher that its copy was still current.
|
|
Updated time.Time `json:"updated"`
|
|
|
|
// VPN says that the list holds servers of VPN services. A lookup answer
|
|
// reports it through is_vpn and vpn_providers.
|
|
VPN bool `json:"-"`
|
|
}
|
|
|
|
// listCopy is a list's last good copy in memory.
|
|
type listCopy struct {
|
|
addresses *netipx.IPSet
|
|
updated time.Time
|
|
|
|
// validators identify the version of the list this copy holds.
|
|
validators validators
|
|
}
|
|
|
|
// Params is what New needs.
|
|
type Params struct {
|
|
// Dir is the directory that holds the copies on disk.
|
|
Dir string
|
|
|
|
// Lists are the lists to download and look addresses up in.
|
|
Lists []List
|
|
|
|
Logger *slog.Logger
|
|
}
|
|
|
|
// IPLists keeps a copy of each list and answers which lists contain an
|
|
// address. Start loads the copies from disk and starts downloading the lists.
|
|
type IPLists struct {
|
|
dir string
|
|
lists []List
|
|
logger *slog.Logger
|
|
|
|
// copies holds each list's last good copy, by list name. A list with no
|
|
// good copy yet has none.
|
|
mu sync.RWMutex
|
|
copies map[string]listCopy
|
|
|
|
// stop ends the goroutine Start runs, and done is closed when it returns.
|
|
stop context.CancelFunc
|
|
done chan struct{}
|
|
}
|
|
|
|
// New returns an IPLists with no copies. Start loads them.
|
|
func New(params Params) *IPLists {
|
|
return &IPLists{
|
|
dir: params.Dir,
|
|
lists: params.Lists,
|
|
logger: params.Logger.With("component", "iplists"),
|
|
copies: make(map[string]listCopy),
|
|
done: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// Lookup returns a Listing for each list whose copy contains addr, in the
|
|
// order of the lists given to New. It makes no outside request. When no list
|
|
// contains addr, it returns an empty list, not nil.
|
|
func (b *IPLists) Lookup(addr netip.Addr) []Listing {
|
|
b.mu.RLock()
|
|
defer b.mu.RUnlock()
|
|
|
|
listings := []Listing{}
|
|
|
|
for _, list := range b.lists {
|
|
c, ok := b.copies[list.Name]
|
|
if ok && c.addresses.Contains(addr.Unmap()) {
|
|
listings = append(listings, Listing{
|
|
Name: list.Name,
|
|
Description: list.Description,
|
|
Updated: c.updated,
|
|
VPN: list.VPN,
|
|
})
|
|
}
|
|
}
|
|
|
|
return listings
|
|
}
|
|
|
|
// ShortestInterval returns the shortest Interval of the lists. That is the
|
|
// soonest a copy can change, and so how long a lookup answer stays current.
|
|
func (b *IPLists) ShortestInterval() time.Duration {
|
|
var shortest time.Duration
|
|
|
|
for i, list := range b.lists {
|
|
if i == 0 || list.Interval < shortest {
|
|
shortest = list.Interval
|
|
}
|
|
}
|
|
|
|
return shortest
|
|
}
|
|
|
|
// updated returns when the named list's copy was last downloaded or found
|
|
// current, or the zero time when the list has no copy.
|
|
func (b *IPLists) updated(name string) time.Time {
|
|
b.mu.RLock()
|
|
defer b.mu.RUnlock()
|
|
|
|
return b.copies[name].updated
|
|
}
|
|
|
|
// copyValidators returns the validators of the named list's copy. They are
|
|
// empty when the list has no copy.
|
|
func (b *IPLists) copyValidators(name string) validators {
|
|
b.mu.RLock()
|
|
defer b.mu.RUnlock()
|
|
|
|
return b.copies[name].validators
|
|
}
|
|
|
|
// setCopy makes c the named list's copy.
|
|
func (b *IPLists) setCopy(name string, c listCopy) {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
b.copies[name] = c
|
|
}
|
|
|
|
// setUpdated records that the named list's copy was found current at t.
|
|
func (b *IPLists) setUpdated(name string, t time.Time) {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
c, ok := b.copies[name]
|
|
if ok {
|
|
c.updated = t
|
|
b.copies[name] = c
|
|
}
|
|
}
|