This saves a lot of memory by allowing the IP blocklist to be mmap()ed in. In production with the latest level1 blocklist it's 35MB per process.
179 lines
3.9 KiB
Go
179 lines
3.9 KiB
Go
// Package iplist handles the P2P Plaintext Format described by
|
|
// https://en.wikipedia.org/wiki/PeerGuardian#P2P_plaintext_format.
|
|
package iplist
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"sort"
|
|
)
|
|
|
|
// An abstraction of IP list implementations.
|
|
type Ranger interface {
|
|
// Return a Range containing the IP.
|
|
Lookup(net.IP) *Range
|
|
// If your ranges hurt, use this.
|
|
NumRanges() int
|
|
}
|
|
|
|
type IPList struct {
|
|
ranges []Range
|
|
}
|
|
|
|
type Range struct {
|
|
First, Last net.IP
|
|
Description string
|
|
}
|
|
|
|
func (r *Range) String() string {
|
|
return fmt.Sprintf("%s-%s (%s)", r.First, r.Last, r.Description)
|
|
}
|
|
|
|
// Create a new IP list. The given ranges must already sorted by the lower
|
|
// bound IP in each range. Behaviour is undefined for lists of overlapping
|
|
// ranges.
|
|
func New(initSorted []Range) *IPList {
|
|
return &IPList{
|
|
ranges: initSorted,
|
|
}
|
|
}
|
|
|
|
func (me *IPList) NumRanges() int {
|
|
if me == nil {
|
|
return 0
|
|
}
|
|
return len(me.ranges)
|
|
}
|
|
|
|
// Return the range the given IP is in. Returns nil if no range is found.
|
|
func (me *IPList) Lookup(ip net.IP) (r *Range) {
|
|
if me == nil {
|
|
return nil
|
|
}
|
|
// TODO: Perhaps all addresses should be converted to IPv6, if the future
|
|
// of IP is to always be backwards compatible. But this will cost 4x the
|
|
// memory for IPv4 addresses?
|
|
v4 := ip.To4()
|
|
if v4 != nil {
|
|
r = me.lookup(v4)
|
|
if r != nil {
|
|
return
|
|
}
|
|
}
|
|
v6 := ip.To16()
|
|
if v6 != nil {
|
|
return me.lookup(v6)
|
|
}
|
|
if v4 == nil && v6 == nil {
|
|
return &Range{
|
|
Description: "bad IP",
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Return a range that contains ip, or nil.
|
|
func lookup(f func(i int) Range, n int, ip net.IP) *Range {
|
|
// Find the index of the first range for which the following range exceeds
|
|
// it.
|
|
i := sort.Search(n, func(i int) bool {
|
|
if i+1 >= n {
|
|
return true
|
|
}
|
|
r := f(i + 1)
|
|
return bytes.Compare(ip, r.First) < 0
|
|
})
|
|
if i == n {
|
|
return nil
|
|
}
|
|
r := f(i)
|
|
if bytes.Compare(ip, r.First) < 0 || bytes.Compare(ip, r.Last) > 0 {
|
|
return nil
|
|
}
|
|
return &r
|
|
}
|
|
|
|
// Return the range the given IP is in. Returns nil if no range is found.
|
|
func (me *IPList) lookup(ip net.IP) (r *Range) {
|
|
return lookup(func(i int) Range {
|
|
return me.ranges[i]
|
|
}, len(me.ranges), ip)
|
|
}
|
|
|
|
func minifyIP(ip *net.IP) {
|
|
v4 := ip.To4()
|
|
if v4 != nil {
|
|
*ip = append(make([]byte, 0, 4), v4...)
|
|
}
|
|
}
|
|
|
|
// Parse a line of the PeerGuardian Text Lists (P2P) Format. Returns !ok but
|
|
// no error if a line doesn't contain a range but isn't erroneous, such as
|
|
// comment and blank lines.
|
|
func ParseBlocklistP2PLine(l []byte) (r Range, ok bool, err error) {
|
|
l = bytes.TrimSpace(l)
|
|
if len(l) == 0 || bytes.HasPrefix(l, []byte("#")) {
|
|
return
|
|
}
|
|
// TODO: Check this when IPv6 blocklists are available.
|
|
colon := bytes.LastIndexAny(l, ":")
|
|
if colon == -1 {
|
|
err = errors.New("missing colon")
|
|
return
|
|
}
|
|
hyphen := bytes.IndexByte(l[colon+1:], '-')
|
|
if hyphen == -1 {
|
|
err = errors.New("missing hyphen")
|
|
return
|
|
}
|
|
hyphen += colon + 1
|
|
r.Description = string(l[:colon])
|
|
r.First = net.ParseIP(string(l[colon+1 : hyphen]))
|
|
minifyIP(&r.First)
|
|
r.Last = net.ParseIP(string(l[hyphen+1:]))
|
|
minifyIP(&r.Last)
|
|
if r.First == nil || r.Last == nil || len(r.First) != len(r.Last) {
|
|
err = errors.New("bad IP range")
|
|
return
|
|
}
|
|
ok = true
|
|
return
|
|
}
|
|
|
|
// Creates an IPList from a line-delimited P2P Plaintext file.
|
|
func NewFromReader(f io.Reader) (ret *IPList, err error) {
|
|
var ranges []Range
|
|
// There's a lot of similar descriptions, so we maintain a pool and reuse
|
|
// them to reduce memory overhead.
|
|
uniqStrs := make(map[string]string)
|
|
scanner := bufio.NewScanner(f)
|
|
lineNum := 1
|
|
for scanner.Scan() {
|
|
r, ok, lineErr := ParseBlocklistP2PLine(scanner.Bytes())
|
|
if lineErr != nil {
|
|
err = fmt.Errorf("error parsing line %d: %s", lineNum, lineErr)
|
|
return
|
|
}
|
|
lineNum++
|
|
if !ok {
|
|
continue
|
|
}
|
|
if s, ok := uniqStrs[r.Description]; ok {
|
|
r.Description = s
|
|
} else {
|
|
uniqStrs[r.Description] = r.Description
|
|
}
|
|
ranges = append(ranges, r)
|
|
}
|
|
err = scanner.Err()
|
|
if err != nil {
|
|
return
|
|
}
|
|
ret = New(ranges)
|
|
return
|
|
}
|