check / check (push) Canceled after 0s
A new package, internal/blocklist, downloads seven public lists on a schedule: DShield, Feodo Tracker, FireHOL abusers and level 3, Spamhaus DROP for IPv4 and IPv6, and the Tor exit list. It keeps the last good copy of each in memory and in the state directory's blocklists directory, and loads the copies at startup. Each list's interval follows its publisher's page, cited in the code. Each IP lookup result carries blocklists, one entry per list the address is on. A successful lookup answer carries Cache-Control, private for the caller's own address. The address sets come from go4.org/netipx. Model: opus-5-5
173 lines
4.3 KiB
Go
173 lines
4.3 KiB
Go
// 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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|