Некоторые проверки не удались
CI / test (push) Successful in 2m5s
Docker / Build and publish worker image (push) Failing after 31s
194 строки
6.2 KiB
Go
194 строки
6.2 KiB
Go
package models
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"strings"
|
|
|
|
"github.com/davecgh/go-spew/spew"
|
|
|
|
"rocketgit.ru/rsmon/worker/app/models/concerns"
|
|
"rocketgit.ru/rsmon/worker/internal/netaddr"
|
|
)
|
|
|
|
// RknIP stores a single CIDR from the ru-blocked IP list. The cidr column
|
|
// uses Postgres's native cidr type so the (>>) containment operator works
|
|
// directly inside IsRknIPBlocked queries — GiST index recommended for any
|
|
// table large enough to amortize the planner cost; see EnsureRknIndexes.
|
|
type RknIP struct {
|
|
concerns.Model
|
|
Network *netaddr.Cidr `json:"network" gorm:"type:cidr;"`
|
|
}
|
|
|
|
// TableName pins the underlying table name.
|
|
func (RknIP) TableName() string { return "rkn_ips" }
|
|
|
|
// PanicOnErr panics with a spew-formatted error dump if err is non-nil.
|
|
// Kept for callers that still use the old add-by-row InsertInBatches path
|
|
// (none after this commit, but kept in case external scripts reference it).
|
|
func PanicOnErr(err error) {
|
|
if err != nil {
|
|
spew.Dump(err)
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
// FindRknIP returns true iff the IP inside `ip` (treated as a /32 or /128
|
|
// host) falls inside any CIDR already stored in rkn_ips. Kept for callers
|
|
// that already construct an internal/netaddr.Inet.
|
|
func FindRknIP(ip netaddr.Inet) (bool, error) {
|
|
ipstr := ip.Inet.String()
|
|
if !strings.Contains(ipstr, "/") {
|
|
if ip.Inet.To4() != nil {
|
|
ipstr += "/32"
|
|
} else {
|
|
ipstr += "/128"
|
|
}
|
|
}
|
|
_, network, err := net.ParseCIDR(ipstr)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
cidr := netaddr.Cidr{Cidr: *network, Valid: true}
|
|
var matched []RknIP
|
|
if err := DB().Raw("SELECT id FROM rkn_ips WHERE network >>= ?", &cidr).Scan(&matched).Error; err != nil {
|
|
return false, err
|
|
}
|
|
return len(matched) > 0, nil
|
|
}
|
|
|
|
// AddRknIP is the legacy per-row insert path. Deprecated: callers should
|
|
// invoke ReplaceRknIPs from the rkn updater. Kept around so existing cron
|
|
// scripts that import the symbol keep compiling.
|
|
func AddRknIP(data []string, count int) {
|
|
tx := DB().Begin()
|
|
for k, dataIP := range data {
|
|
if count > 0 && k > count-1 {
|
|
break
|
|
}
|
|
if !strings.Contains(dataIP, "/") {
|
|
dataIP += "/32"
|
|
}
|
|
_, network, err := net.ParseCIDR(dataIP)
|
|
PanicOnErr(err)
|
|
|
|
cidr := netaddr.Cidr{Cidr: *network, Valid: true}
|
|
var rknIPs []RknIP
|
|
err = tx.Raw("SELECT id FROM rkn_ips where network >>= ?", &cidr).Scan(&rknIPs).Error
|
|
PanicOnErr(err)
|
|
if len(rknIPs) == 0 {
|
|
rknIPs = []RknIP{}
|
|
err = tx.Raw("SELECT id FROM rkn_ips where network << ?", &cidr).Scan(&rknIPs).Error
|
|
PanicOnErr(err)
|
|
if len(rknIPs) > 0 {
|
|
for _, r := range rknIPs {
|
|
PanicOnErr(tx.Delete(&r).Error)
|
|
}
|
|
}
|
|
PanicOnErr(tx.Create(&RknIP{Network: &cidr}).Error)
|
|
}
|
|
log.Println("created:", dataIP)
|
|
}
|
|
PanicOnErr(tx.Commit().Error)
|
|
}
|
|
|
|
// ReplaceRknIPs deletes every existing row and bulk-inserts the given CIDRs
|
|
// in batches of 1000. Whole operation runs in a single transaction so a
|
|
// partially-applied update can never leave the table in a hybrid state.
|
|
//
|
|
// CIDR.parseCIDR-loop uses net.ParseCIDR to canonicalise the address —
|
|
// upstream .dat files occasionally contain range/mask pairs that aren't
|
|
// already reduced (e.g. 192.168.0.0/16 spelled as 192.168.5.0/16); the
|
|
// Postgres cidr type normalises on insert.
|
|
func ReplaceRknIPs(nets []*net.IPNet) error {
|
|
tx := DB().Begin()
|
|
if tx.Error != nil {
|
|
return tx.Error
|
|
}
|
|
|
|
rows := make([]RknIP, 0, len(nets))
|
|
seen := make(map[string]struct{}, len(nets))
|
|
for _, n := range nets {
|
|
if n == nil || n.IP == nil {
|
|
continue
|
|
}
|
|
// Canonicalise by routing through net.ParseCIDR. This drops the
|
|
// host bits (a common bug in upstream dumps where a /24 range
|
|
// is written with the .5 host bit set) and stamps the right
|
|
// address family flag for Postgres.
|
|
canonical := n.String()
|
|
if _, parsed, err := net.ParseCIDR(canonical); err == nil {
|
|
n = parsed
|
|
canonical = parsed.String()
|
|
}
|
|
if _, ok := seen[canonical]; ok {
|
|
continue
|
|
}
|
|
seen[canonical] = struct{}{}
|
|
cidr := netaddr.Cidr{Cidr: *n, Valid: true}
|
|
rows = append(rows, RknIP{Network: &cidr})
|
|
}
|
|
|
|
// Wipe + re-insert in one transaction.
|
|
if err := tx.Where("1 = 1").Delete(&RknIP{}).Error; err != nil {
|
|
_ = tx.Rollback().Error
|
|
return err
|
|
}
|
|
const batchSize = 1000
|
|
for start := 0; start < len(rows); start += batchSize {
|
|
end := start + batchSize
|
|
if end > len(rows) {
|
|
end = len(rows)
|
|
}
|
|
if err := tx.CreateInBatches(rows[start:end], batchSize).Error; err != nil {
|
|
_ = tx.Rollback().Error
|
|
return err
|
|
}
|
|
}
|
|
return tx.Commit().Error
|
|
}
|
|
|
|
// IsRknIPBlocked returns true iff `ip` (any textual form ParseCIDR accepts)
|
|
// falls inside any CIDR stored in the rkn_ips table. The query uses the
|
|
// cidr >>= inet containment operator — see EnsureRknIndexes for the GiST
|
|
// index that makes this fast at scale.
|
|
func IsRknIPBlocked(ip string) (bool, error) {
|
|
ip = strings.TrimSpace(ip)
|
|
if ip == "" {
|
|
return false, nil
|
|
}
|
|
var count int64
|
|
if err := DB().Raw("SELECT COUNT(*) FROM rkn_ips WHERE network >>= ?::inet", ip).Scan(&count).Error; err != nil {
|
|
return false, err
|
|
}
|
|
return count > 0, nil
|
|
}
|
|
|
|
// EnsureRknIndexes creates the indexes that AutoMigrate cannot express —
|
|
// the GiST index on rkn_ips.network uses cidr >>= cidr containment (the
|
|
// expression index `network` already covers equality and prefix ranges,
|
|
// but the planner benefits from a GiST for `network >>= <other cidr>`
|
|
// queries against ~30k rows). The unique index on rkn_domains.domain is
|
|
// also declared in the GORM tag, this function only adds what GORM can't
|
|
// (GiST) and is idempotent so it's safe to call repeatedly during boot
|
|
// or migration.
|
|
func EnsureRknIndexes() error {
|
|
// GiST on cidr requires the btree_gist contrib — its `cidr_ops`
|
|
// opclass exposes cidr to GiST. CREATE EXTENSION IF NOT EXISTS is
|
|
// idempotent.
|
|
if err := DB().Exec("CREATE EXTENSION IF NOT EXISTS btree_gist").Error; err != nil {
|
|
return fmt.Errorf("ensure btree_gist: %w", err)
|
|
}
|
|
// rkn_ips GiST index on the cidr column supports the >>= containment
|
|
// operator that IsRknIPBlocked uses. Without it a 30k-row table makes
|
|
// every IP check a sequential scan; with it each check is an index
|
|
// probe.
|
|
if err := DB().Exec(
|
|
"CREATE INDEX IF NOT EXISTS idx_rkn_ips_network ON rkn_ips USING gist (network)",
|
|
).Error; err != nil {
|
|
return fmt.Errorf("ensure idx_rkn_ips_network: %w", err)
|
|
}
|
|
return nil
|
|
}
|