mirror of
https://github.com/muun/recovery.git
synced 2025-02-23 11:32:33 -05:00
222 lines
6.3 KiB
Go
222 lines
6.3 KiB
Go
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)
|
|
}
|