feat(scan): диапазон IPv4-подсетей CIDR–CIDR в поле целей (нормализация в список)
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
package scans
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// maxCIDRSExpanded — ограничение на число подсетей после раскрытия диапазонов (защита от опечаток).
|
||||
const maxCIDRSExpanded = 16384
|
||||
|
||||
var cidrRangeRE = regexp.MustCompile(`^\s*(?P<left>.+?/\d+)\s*-\s*(?P<right>.+?/\d+)\s*$`)
|
||||
|
||||
// expandCIDRSFromRequest раскрывает каждую строку: один CIDR или диапазон "start/end mask - end/end mask".
|
||||
func expandCIDRSFromRequest(inputs []string) ([]string, error) {
|
||||
var all []string
|
||||
for _, raw := range inputs {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
continue
|
||||
}
|
||||
part, err := expandCIDREntry(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
all = append(all, part...)
|
||||
}
|
||||
if len(all) == 0 {
|
||||
return nil, errors.New("cidrs must not be empty")
|
||||
}
|
||||
seen := make(map[string]struct{}, len(all))
|
||||
var out []string
|
||||
for _, c := range all {
|
||||
if _, ok := seen[c]; ok {
|
||||
continue
|
||||
}
|
||||
seen[c] = struct{}{}
|
||||
out = append(out, c)
|
||||
}
|
||||
if len(out) > maxCIDRSExpanded {
|
||||
return nil, fmt.Errorf("слишком много подсетей после раскрытия (%d), максимум %d", len(out), maxCIDRSExpanded)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func expandCIDREntry(s string) ([]string, error) {
|
||||
if m := cidrRangeRE.FindStringSubmatch(s); m != nil {
|
||||
left, right := strings.TrimSpace(m[1]), strings.TrimSpace(m[2])
|
||||
return expandCIDRRange(left, right)
|
||||
}
|
||||
if _, _, err := net.ParseCIDR(strings.TrimSpace(s)); err != nil {
|
||||
return nil, fmt.Errorf("некорректный CIDR: %w", err)
|
||||
}
|
||||
return []string{strings.TrimSpace(s)}, nil
|
||||
}
|
||||
|
||||
func expandCIDRRange(a, b string) ([]string, error) {
|
||||
_, na, err := net.ParseCIDR(a)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("начало диапазона: %w", err)
|
||||
}
|
||||
_, nb, err := net.ParseCIDR(b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("конец диапазона: %w", err)
|
||||
}
|
||||
|
||||
bitsA, _ := na.Mask.Size()
|
||||
bitsB, _ := nb.Mask.Size()
|
||||
if bitsA != bitsB {
|
||||
return nil, fmt.Errorf("маски начала и конца должны совпадать (/ %d и / %d)", bitsA, bitsB)
|
||||
}
|
||||
ones := bitsA
|
||||
if ones < 0 || ones > 32 {
|
||||
return nil, errors.New("диапазон сетей поддерживается только для IPv4 с маской /0–/32")
|
||||
}
|
||||
|
||||
ipA := na.IP.To4()
|
||||
ipB := nb.IP.To4()
|
||||
if ipA == nil || ipB == nil {
|
||||
return nil, errors.New("диапазон вида CIDR–CIDR пока поддерживается только для IPv4")
|
||||
}
|
||||
|
||||
netA := ipv4NetworkBase(ipA, na.Mask)
|
||||
netB := ipv4NetworkBase(ipB, nb.Mask)
|
||||
sa := binary.BigEndian.Uint32(netA)
|
||||
sb := binary.BigEndian.Uint32(netB)
|
||||
if sb < sa {
|
||||
return nil, errors.New("конечная сеть в диапазоне раньше начальной")
|
||||
}
|
||||
|
||||
var step uint64 = 1 << (32 - uint(ones))
|
||||
if ones == 32 {
|
||||
step = 1
|
||||
}
|
||||
if uint64(sa)%step != 0 || uint64(sb)%step != 0 {
|
||||
return nil, errors.New("границы диапазона должны быть адресами сетей (кратны размеру подсети)")
|
||||
}
|
||||
|
||||
var out []string
|
||||
for cur := uint64(sa); cur <= uint64(sb); cur += step {
|
||||
if len(out) >= maxCIDRSExpanded {
|
||||
return nil, fmt.Errorf("диапазон даёт больше %d подсетей", maxCIDRSExpanded)
|
||||
}
|
||||
ip := make(net.IP, 4)
|
||||
binary.BigEndian.PutUint32(ip, uint32(cur))
|
||||
out = append(out, fmt.Sprintf("%s/%d", ip.String(), ones))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func ipv4NetworkBase(ip net.IP, mask net.IPMask) net.IP {
|
||||
n := ip.Mask(mask)
|
||||
out := make(net.IP, 4)
|
||||
copy(out, n.To4())
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user