// Package blocklist downloads public IP blocklists on a schedule, keeps the // last good copy of each on disk and in memory, and answers which lists // contain an address. package blocklist 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 ) // List is one public blocklist. 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 } // 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"` } // 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 } // Blocklists 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 Blocklists 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 a Blocklists with no copies. Start loads them. func New(params Params) *Blocklists { return &Blocklists{ dir: params.Dir, lists: params.Lists, logger: params.Logger.With("component", "blocklists"), 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 *Blocklists) 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, }) } } 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 *Blocklists) 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 *Blocklists) 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 *Blocklists) 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 *Blocklists) 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 *Blocklists) 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 } }