Release v0.1.0

This commit is contained in:
Manu Herrera
2019-10-01 12:22:30 -03:00
parent 41e6aad190
commit d301c63596
915 changed files with 378049 additions and 11 deletions

View File

@@ -0,0 +1,83 @@
package banman
import (
"io"
"net"
)
// ipType represents the different types of IP addresses supported by the
// BanStore interface.
type ipType = byte
const (
// ipv4 represents an IP address of type IPv4.
ipv4 ipType = 0
// ipv6 represents an IP address of type IPv6.
ipv6 ipType = 1
)
// encodeIPNet serializes the IP network into the given reader.
func encodeIPNet(w io.Writer, ipNet *net.IPNet) error {
// Determine the appropriate IP type for the IP address contained in the
// network.
var (
ip []byte
ipType ipType
)
switch {
case ipNet.IP.To4() != nil:
ip = ipNet.IP.To4()
ipType = ipv4
case ipNet.IP.To16() != nil:
ip = ipNet.IP.To16()
ipType = ipv6
default:
return ErrUnsupportedIP
}
// Write the IP type first in order to properly identify it when
// deserializing it, followed by the IP itself and its mask.
if _, err := w.Write([]byte{ipType}); err != nil {
return err
}
if _, err := w.Write(ip); err != nil {
return err
}
if _, err := w.Write([]byte(ipNet.Mask)); err != nil {
return err
}
return nil
}
// decodeIPNet deserialized an IP network from the given reader.
func decodeIPNet(r io.Reader) (*net.IPNet, error) {
// Read the IP address type and determine whether it is supported.
var ipType [1]byte
if _, err := r.Read(ipType[:]); err != nil {
return nil, err
}
var ipLen int
switch ipType[0] {
case ipv4:
ipLen = net.IPv4len
case ipv6:
ipLen = net.IPv6len
default:
return nil, ErrUnsupportedIP
}
// Once we have the type and its corresponding length, attempt to read
// it and its mask.
ip := make([]byte, ipLen)
if _, err := r.Read(ip[:]); err != nil {
return nil, err
}
mask := make([]byte, ipLen)
if _, err := r.Read(mask[:]); err != nil {
return nil, err
}
return &net.IPNet{IP: ip, Mask: mask}, nil
}

View File

@@ -0,0 +1,43 @@
package banman
// Reason includes the different possible reasons which caused us to ban a peer.
type Reason uint8
// We prevent using `iota` to ensure the order does not have the value since
// these are serialized within the database.
const (
// ExcedeedBanThreshold signals that a peer exceeded its ban threshold.
ExceededBanThreshold Reason = 1
// NoCompactFilters signals that a peer was unable to serve us compact
// filters.
NoCompactFilters Reason = 2
// InvalidFilterHeader signals that a peer served us an invalid filter
// header.
InvalidFilterHeader Reason = 3
// InvalidFilterHeaderCheckpoint signals that a peer served us an
// invalid filter header checkpoint.
InvalidFilterHeaderCheckpoint Reason = 4
)
// String returns a human-readable description for the reason a peer was banned.
func (r Reason) String() string {
switch r {
case ExceededBanThreshold:
return "peer exceeded ban threshold"
case NoCompactFilters:
return "peer was unable to serve compact filters"
case InvalidFilterHeader:
return "peer served invalid filter header"
case InvalidFilterHeaderCheckpoint:
return "peer served invalid filter header checkpoint"
default:
return "unknown reason"
}
}

View File

@@ -0,0 +1,221 @@
package banman
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"net"
"time"
"github.com/btcsuite/btcwallet/walletdb"
)
var (
// byteOrder is the preferred byte order in which we should write things
// to disk.
byteOrder = binary.BigEndian
// banStoreBucket is the top level bucket of the Store that will contain
// all relevant sub-buckets.
banStoreBucket = []byte("ban-store")
// banBucket is the main index in which we keep track of IP networks and
// their absolute expiration time.
//
// The key is the IP network host and the value is the absolute
// expiration time.
banBucket = []byte("ban-index")
// reasonBucket is an index in which we keep track of why an IP network
// was banned.
//
// The key is the IP network and the value is the Reason.
reasonBucket = []byte("reason-index")
// ErrCorruptedStore is an error returned when we attempt to locate any
// of the ban-related buckets in the database but are unable to.
ErrCorruptedStore = errors.New("corrupted ban store")
// ErrUnsupportedIP is an error returned when we attempt to parse an
// unsupported IP address type.
ErrUnsupportedIP = errors.New("unsupported IP type")
)
// Status gathers all of the details regarding an IP network's ban status.
type Status struct {
// Banned determines whether the IP network is currently banned.
Banned bool
// Reason is the reason for which the IP network was banned.
Reason Reason
// Expiration is the absolute time in which the ban will expire.
Expiration time.Time
}
// Store is the store responsible for maintaining records of banned IP networks.
// It uses IP networks, rather than single IP addresses, in order to coalesce
// multiple IP addresses that are likely to be correlated.
type Store interface {
// BanIPNet creates a ban record for the IP network within the store for
// the given duration. A reason can also be provided to note why the IP
// network is being banned. The record will exist until a call to Status
// is made after the ban expiration.
BanIPNet(*net.IPNet, Reason, time.Duration) error
// Status returns the ban status for a given IP network.
Status(*net.IPNet) (Status, error)
}
// NewStore returns a Store backed by a database.
func NewStore(db walletdb.DB) (Store, error) {
return newBanStore(db)
}
// banStore is a concrete implementation of the Store interface backed by a
// database.
type banStore struct {
db walletdb.DB
}
// A compile-time constraint to ensure banStore satisfies the Store interface.
var _ Store = (*banStore)(nil)
// newBanStore creates a concrete implementation of the Store interface backed
// by a database.
func newBanStore(db walletdb.DB) (*banStore, error) {
s := &banStore{db: db}
// We'll ensure the expected buckets are created upon initialization.
err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
banStore, err := tx.CreateTopLevelBucket(banStoreBucket)
if err != nil {
return err
}
_, err = banStore.CreateBucketIfNotExists(banBucket)
if err != nil {
return err
}
_, err = banStore.CreateBucketIfNotExists(reasonBucket)
return err
})
if err != nil && err != walletdb.ErrBucketExists {
return nil, err
}
return s, nil
}
// BanIPNet creates a ban record for the IP network within the store for the
// given duration. A reason can also be provided to note why the IP network is
// being banned. The record will exist until a call to Status is made after the
// ban expiration.
func (s *banStore) BanIPNet(ipNet *net.IPNet, reason Reason, duration time.Duration) error {
return walletdb.Update(s.db, func(tx walletdb.ReadWriteTx) error {
banStore := tx.ReadWriteBucket(banStoreBucket)
if banStore == nil {
return ErrCorruptedStore
}
banIndex := banStore.NestedReadWriteBucket(banBucket)
if banIndex == nil {
return ErrCorruptedStore
}
reasonIndex := banStore.NestedReadWriteBucket(reasonBucket)
if reasonIndex == nil {
return ErrCorruptedStore
}
var ipNetBuf bytes.Buffer
if err := encodeIPNet(&ipNetBuf, ipNet); err != nil {
return fmt.Errorf("unable to encode %v: %v", ipNet, err)
}
k := ipNetBuf.Bytes()
return addBannedIPNet(banIndex, reasonIndex, k, reason, duration)
})
}
// addBannedIPNet adds an entry to the ban store for the given IP network.
func addBannedIPNet(banIndex, reasonIndex walletdb.ReadWriteBucket,
ipNetKey []byte, reason Reason, duration time.Duration) error {
var v [8]byte
banExpiration := time.Now().Add(duration)
byteOrder.PutUint64(v[:], uint64(banExpiration.Unix()))
if err := banIndex.Put(ipNetKey, v[:]); err != nil {
return err
}
return reasonIndex.Put(ipNetKey, []byte{byte(reason)})
}
// Status returns the ban status for a given IP network.
func (s *banStore) Status(ipNet *net.IPNet) (Status, error) {
var banStatus Status
err := walletdb.Update(s.db, func(tx walletdb.ReadWriteTx) error {
banStore := tx.ReadWriteBucket(banStoreBucket)
if banStore == nil {
return ErrCorruptedStore
}
banIndex := banStore.NestedReadWriteBucket(banBucket)
if banIndex == nil {
return ErrCorruptedStore
}
reasonIndex := banStore.NestedReadWriteBucket(reasonBucket)
if reasonIndex == nil {
return ErrCorruptedStore
}
var ipNetBuf bytes.Buffer
if err := encodeIPNet(&ipNetBuf, ipNet); err != nil {
return fmt.Errorf("unable to encode %v: %v", ipNet, err)
}
k := ipNetBuf.Bytes()
status := fetchStatus(banIndex, reasonIndex, k)
// If the IP network's ban duration has expired, we can remove
// its entry from the store.
if !time.Now().Before(status.Expiration) {
return removeBannedIPNet(banIndex, reasonIndex, k)
}
banStatus = status
return nil
})
if err != nil {
return Status{}, err
}
return banStatus, nil
}
// fetchStatus retrieves the ban status of the given IP network.
func fetchStatus(banIndex, reasonIndex walletdb.ReadWriteBucket,
ipNetKey []byte) Status {
v := banIndex.Get(ipNetKey)
if v == nil {
return Status{}
}
reason := Reason(reasonIndex.Get(ipNetKey)[0])
banExpiration := time.Unix(int64(byteOrder.Uint64(v)), 0)
return Status{
Banned: true,
Reason: reason,
Expiration: banExpiration,
}
}
// removeBannedIPNet removes all references to a banned IP network within the
// ban store.
func removeBannedIPNet(banIndex, reasonIndex walletdb.ReadWriteBucket,
ipNetKey []byte) error {
if err := banIndex.Delete(ipNetKey); err != nil {
return err
}
return reasonIndex.Delete(ipNetKey)
}

View File

@@ -0,0 +1,48 @@
package banman
import (
"net"
)
var (
// defaultIPv4Mask is the default IPv4 mask used when parsing IP
// networks from an address. This ensures that the IP network only
// contains *one* IP address -- the one specified.
defaultIPv4Mask = net.CIDRMask(32, 32)
// defaultIPv6Mask is the default IPv6 mask used when parsing IP
// networks from an address. This ensures that the IP network only
// contains *one* IP address -- the one specified.
defaultIPv6Mask = net.CIDRMask(128, 128)
)
// ParseIPNet parses the IP network that contains the given address. An optional
// mask can be provided, to expand the scope of the IP network, otherwise the
// IP's default is used.
//
// NOTE: This assumes that the address has already been resolved.
func ParseIPNet(addr string, mask net.IPMask) (*net.IPNet, error) {
// If the address includes a port, we'll remove it.
host, _, err := net.SplitHostPort(addr)
if err != nil {
// Address doesn't include a port.
host = addr
}
// Parse the IP from the host to ensure it is supported.
ip := net.ParseIP(host)
switch {
case ip.To4() != nil:
if mask == nil {
mask = defaultIPv4Mask
}
case ip.To16() != nil:
if mask == nil {
mask = defaultIPv6Mask
}
default:
return nil, ErrUnsupportedIP
}
return &net.IPNet{IP: ip.Mask(mask), Mask: mask}, nil
}