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
159 lines
3.5 KiB
Go
159 lines
3.5 KiB
Go
package blocklist
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/netip"
|
|
"strings"
|
|
|
|
"go4.org/netipx"
|
|
)
|
|
|
|
const (
|
|
// commentPrefix starts a comment line in a list file.
|
|
commentPrefix = "#"
|
|
|
|
// networkSeparator separates the address and the mask length of a
|
|
// network, as in 192.0.2.0/24.
|
|
networkSeparator = "/"
|
|
|
|
// dshieldFieldSeparator separates the fields of a DShield line.
|
|
dshieldFieldSeparator = "\t"
|
|
|
|
// spamhausMetadataType is the type of the line that ends a Spamhaus
|
|
// file. That line holds the file's metadata and no network.
|
|
spamhausMetadataType = "metadata"
|
|
)
|
|
|
|
var (
|
|
errUnknownFormat = errors.New("unknown list format")
|
|
errNoNetwork = errors.New("no network on the line")
|
|
errBadRange = errors.New("first and last address make no range")
|
|
)
|
|
|
|
// parse reads a list file written in format and returns the addresses it
|
|
// holds. A line it cannot read fails the whole file, so a file that is not
|
|
// the list, such as an error page, never replaces a good copy.
|
|
func parse(r io.Reader, format Format) (*netipx.IPSet, error) {
|
|
var builder netipx.IPSetBuilder
|
|
|
|
scanner := bufio.NewScanner(r)
|
|
|
|
for lineNumber := 1; scanner.Scan(); lineNumber++ {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" || strings.HasPrefix(line, commentPrefix) {
|
|
continue
|
|
}
|
|
|
|
err := addLine(&builder, line, format)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("line %d: %w", lineNumber, err)
|
|
}
|
|
}
|
|
|
|
err := scanner.Err()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return builder.IPSet()
|
|
}
|
|
|
|
// addLine adds the address or network on one line of a list file to builder.
|
|
func addLine(builder *netipx.IPSetBuilder, line string, format Format) error {
|
|
switch format {
|
|
case FormatLines:
|
|
return addAddressOrNetwork(builder, line)
|
|
case FormatSpamhausJSON:
|
|
return addSpamhausNetwork(builder, line)
|
|
case FormatDShield:
|
|
return addDShieldNetwork(builder, line)
|
|
default:
|
|
return errUnknownFormat
|
|
}
|
|
}
|
|
|
|
// addAddressOrNetwork adds an address, such as 192.0.2.1, or a network, such
|
|
// as 192.0.2.0/24, to builder.
|
|
func addAddressOrNetwork(builder *netipx.IPSetBuilder, text string) error {
|
|
if strings.Contains(text, networkSeparator) {
|
|
network, err := netip.ParsePrefix(text)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
builder.AddPrefix(network)
|
|
|
|
return nil
|
|
}
|
|
|
|
addr, err := netip.ParseAddr(text)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
builder.Add(addr)
|
|
|
|
return nil
|
|
}
|
|
|
|
// addSpamhausNetwork adds the network on a line of a Spamhaus file to
|
|
// builder. The metadata line adds nothing.
|
|
func addSpamhausNetwork(builder *netipx.IPSetBuilder, line string) error {
|
|
var entry struct {
|
|
CIDR string `json:"cidr"`
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
err := json.Unmarshal([]byte(line), &entry)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if entry.Type == spamhausMetadataType {
|
|
return nil
|
|
}
|
|
|
|
network, err := netip.ParsePrefix(entry.CIDR)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
builder.AddPrefix(network)
|
|
|
|
return nil
|
|
}
|
|
|
|
// addDShieldNetwork adds the network on a line of a DShield file to builder.
|
|
// The line's first two fields are the network's first and last address.
|
|
func addDShieldNetwork(builder *netipx.IPSetBuilder, line string) error {
|
|
firstField, rest, found := strings.Cut(line, dshieldFieldSeparator)
|
|
if !found {
|
|
return errNoNetwork
|
|
}
|
|
|
|
lastField, _, _ := strings.Cut(rest, dshieldFieldSeparator)
|
|
|
|
first, err := netip.ParseAddr(firstField)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
last, err := netip.ParseAddr(lastField)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
network := netipx.IPRangeFrom(first, last)
|
|
if !network.IsValid() {
|
|
return errBadRange
|
|
}
|
|
|
|
builder.AddRange(network)
|
|
|
|
return nil
|
|
}
|