// 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 } }