+41
@@ -0,0 +1,41 @@
|
||||
package iplist
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net"
|
||||
)
|
||||
|
||||
func ParseCIDRListReader(r io.Reader) (ret []Range, err error) {
|
||||
s := bufio.NewScanner(r)
|
||||
for s.Scan() {
|
||||
err = func() (err error) {
|
||||
_, in, err := net.ParseCIDR(s.Text())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ret = append(ret, Range{
|
||||
First: in.IP,
|
||||
Last: IPNetLast(in),
|
||||
})
|
||||
return
|
||||
}()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Returns the last, inclusive IP in a net.IPNet.
|
||||
func IPNetLast(in *net.IPNet) (last net.IP) {
|
||||
n := len(in.IP)
|
||||
if n != len(in.Mask) {
|
||||
panic("wat")
|
||||
}
|
||||
last = make(net.IP, n)
|
||||
for i := 0; i < n; i++ {
|
||||
last[i] = in.IP[i] | ^in.Mask[i]
|
||||
}
|
||||
return
|
||||
}
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
// 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) (r Range, ok bool)
|
||||
// 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 (ipl *IPList) NumRanges() int {
|
||||
if ipl == nil {
|
||||
return 0
|
||||
}
|
||||
return len(ipl.ranges)
|
||||
}
|
||||
|
||||
// Return the range the given IP is in. ok if false if no range is found.
|
||||
func (ipl *IPList) Lookup(ip net.IP) (r Range, ok bool) {
|
||||
if ipl == nil {
|
||||
return
|
||||
}
|
||||
// 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, ok = ipl.lookup(v4)
|
||||
if ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
v6 := ip.To16()
|
||||
if v6 != nil {
|
||||
return ipl.lookup(v6)
|
||||
}
|
||||
if v4 == nil && v6 == nil {
|
||||
r = Range{
|
||||
Description: "bad IP",
|
||||
}
|
||||
ok = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Return a range that contains ip, or nil.
|
||||
func lookup(
|
||||
first func(i int) net.IP,
|
||||
full func(i int) Range,
|
||||
n int,
|
||||
ip net.IP,
|
||||
) (
|
||||
r Range, ok bool,
|
||||
) {
|
||||
// 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
|
||||
}
|
||||
return bytes.Compare(ip, first(i+1)) < 0
|
||||
})
|
||||
if i == n {
|
||||
return
|
||||
}
|
||||
r = full(i)
|
||||
ok = bytes.Compare(r.First, ip) <= 0 && bytes.Compare(ip, r.Last) <= 0
|
||||
return
|
||||
}
|
||||
|
||||
// Return the range the given IP is in. Returns nil if no range is found.
|
||||
func (ipl *IPList) lookup(ip net.IP) (Range, bool) {
|
||||
return lookup(func(i int) net.IP {
|
||||
return ipl.ranges[i].First
|
||||
}, func(i int) Range {
|
||||
return ipl.ranges[i]
|
||||
}, len(ipl.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
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
//go:build !wasm
|
||||
// +build !wasm
|
||||
|
||||
package iplist
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/edsrzf/mmap-go"
|
||||
)
|
||||
|
||||
// The packed format is an 8 byte integer of the number of ranges. Then 20
|
||||
// bytes per range, consisting of 4 byte packed IP being the lower bound IP of
|
||||
// the range, then 4 bytes of the upper, inclusive bound, 8 bytes for the
|
||||
// offset of the description from the end of the packed ranges, and 4 bytes
|
||||
// for the length of the description. After these packed ranges, are the
|
||||
// concatenated descriptions.
|
||||
|
||||
const (
|
||||
packedRangesOffset = 8
|
||||
packedRangeLen = 44
|
||||
)
|
||||
|
||||
func (ipl *IPList) WritePacked(w io.Writer) (err error) {
|
||||
descOffsets := make(map[string]int64, len(ipl.ranges))
|
||||
descs := make([]string, 0, len(ipl.ranges))
|
||||
var nextOffset int64
|
||||
// This is a little monadic, no?
|
||||
write := func(b []byte, expectedLen int) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var n int
|
||||
n, err = w.Write(b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if n != expectedLen {
|
||||
panic(n)
|
||||
}
|
||||
}
|
||||
var b [8]byte
|
||||
binary.LittleEndian.PutUint64(b[:], uint64(len(ipl.ranges)))
|
||||
write(b[:], 8)
|
||||
for _, r := range ipl.ranges {
|
||||
write(r.First.To16(), 16)
|
||||
write(r.Last.To16(), 16)
|
||||
descOff, ok := descOffsets[r.Description]
|
||||
if !ok {
|
||||
descOff = nextOffset
|
||||
descOffsets[r.Description] = descOff
|
||||
descs = append(descs, r.Description)
|
||||
nextOffset += int64(len(r.Description))
|
||||
}
|
||||
binary.LittleEndian.PutUint64(b[:], uint64(descOff))
|
||||
write(b[:], 8)
|
||||
binary.LittleEndian.PutUint32(b[:], uint32(len(r.Description)))
|
||||
write(b[:4], 4)
|
||||
}
|
||||
for _, d := range descs {
|
||||
write([]byte(d), len(d))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func NewFromPacked(b []byte) PackedIPList {
|
||||
ret := PackedIPList(b)
|
||||
minLen := packedRangesOffset + ret.len()*packedRangeLen
|
||||
if len(b) < minLen {
|
||||
panic(fmt.Sprintf("packed len %d < %d", len(b), minLen))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
type PackedIPList []byte
|
||||
|
||||
var _ Ranger = PackedIPList{}
|
||||
|
||||
func (pil PackedIPList) len() int {
|
||||
return int(binary.LittleEndian.Uint64(pil[:8]))
|
||||
}
|
||||
|
||||
func (pil PackedIPList) NumRanges() int {
|
||||
return pil.len()
|
||||
}
|
||||
|
||||
func (pil PackedIPList) getFirst(i int) net.IP {
|
||||
off := packedRangesOffset + packedRangeLen*i
|
||||
return net.IP(pil[off : off+16])
|
||||
}
|
||||
|
||||
func (pil PackedIPList) getRange(i int) (ret Range) {
|
||||
rOff := packedRangesOffset + packedRangeLen*i
|
||||
last := pil[rOff+16 : rOff+32]
|
||||
descOff := int(binary.LittleEndian.Uint64(pil[rOff+32:]))
|
||||
descLen := int(binary.LittleEndian.Uint32(pil[rOff+40:]))
|
||||
descOff += packedRangesOffset + packedRangeLen*pil.len()
|
||||
ret = Range{
|
||||
pil.getFirst(i),
|
||||
net.IP(last),
|
||||
string(pil[descOff : descOff+descLen]),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (pil PackedIPList) Lookup(ip net.IP) (r Range, ok bool) {
|
||||
ip16 := ip.To16()
|
||||
if ip16 == nil {
|
||||
panic(ip)
|
||||
}
|
||||
return lookup(pil.getFirst, pil.getRange, pil.len(), ip16)
|
||||
}
|
||||
|
||||
type closerFunc func() error
|
||||
|
||||
func (me closerFunc) Close() error {
|
||||
return me()
|
||||
}
|
||||
|
||||
func MMapPackedFile(filename string) (
|
||||
ret interface {
|
||||
Ranger
|
||||
io.Closer
|
||||
},
|
||||
err error,
|
||||
) {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
mm, err := mmap.Map(f, mmap.RDONLY, 0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ret = struct {
|
||||
Ranger
|
||||
io.Closer
|
||||
}{NewFromPacked(mm), closerFunc(mm.Unmap)}
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user