445 lines
11 KiB
Go
445 lines
11 KiB
Go
// # IP List Administration
|
|
//
|
|
// The IP List section provides specialized types and methods for handling CommuniGate Pro
|
|
// Network Lists (Client IP addresses, Blacklists, etc.). It implements the unique
|
|
// CommuniGate Pro format that allows for mixed IP addresses, CIDR masks, ranges, and sequences
|
|
// within a single list, including support for negative (exclusion) rules and comments.
|
|
//
|
|
// Key capabilities include:
|
|
// - Flexible Management: adding, inserting, and deleting entries with [Cli.Add],
|
|
// [Cli.InsertBefore], and [Cli.InsertAfter] to maintain strict rule ordering.
|
|
// - Rule Logic: full support for exclusion rules (prefixed with "!") and complex
|
|
// entry types like [IPRange] and [IPSeq].
|
|
// - IP Matching: the [IPList.Contains] method allows for high-performance
|
|
// checks to determine if a specific net.IP is matched by any rule in the list.
|
|
// - Format Preservation: methods for parsing and writing lists while maintaining
|
|
// comment alignment and special character encoding required by the CommuniGate Pro CLI.
|
|
package cgpcli
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// IPCIDR is a wrapper around net.IPNet that represents an IP address with its network mask.
|
|
// It is used to handle CommuniGate Pro's IP and CIDR formats, providing specialized
|
|
// string representation where /32 (IPv4) or /128 (IPv6) masks are omitted if they
|
|
// represent a single host.
|
|
type IPCIDR struct{ net.IPNet }
|
|
|
|
// IPEntry represents a single line in a CommuniGate Pro IP List, including the value,
|
|
// optional comment, and whether it is an exclusion rule.
|
|
type IPEntry struct {
|
|
Comment string // optional comment after the semicolon.
|
|
CommentPos int // original position of the semicolon for alignment.
|
|
IsNegative bool // true if the entry starts with "!" (exclusion rule).
|
|
Value any // the IP value: IPCIDR, IPRange, IPSeq, or IPUnknown.
|
|
}
|
|
|
|
type (
|
|
IPList []IPEntry
|
|
IPRange struct{ Start, End net.IP } // contiguous range of IP addresses from Start to End.
|
|
IPSeq []any // comma-separated sequence of multiple IP rules.
|
|
IPUnknown string
|
|
)
|
|
|
|
// Add appends a new entry to the IP list or updates the comment/exclusion status if the entry exists.
|
|
//
|
|
// Parameters:
|
|
// - input: the IP address, CIDR, range, or sequence to add. Supports "!" prefix for negative rules.
|
|
// - comment: the comment string to associate with this entry.
|
|
//
|
|
// Returns:
|
|
// - error: an error if the input format is invalid.
|
|
func (l *IPList) Add(input string, comment string) error {
|
|
isNegative := false
|
|
trimmedInput := strings.TrimSpace(input)
|
|
|
|
// Распознаем "!" в начале входной строки
|
|
if strings.HasPrefix(trimmedInput, "!") {
|
|
isNegative = true
|
|
trimmedInput = strings.TrimSpace(trimmedInput[1:])
|
|
}
|
|
|
|
val := parseDataPart(trimmedInput)
|
|
if _, ok := val.(IPUnknown); ok {
|
|
return fmt.Errorf("invalid IP format: %s", input)
|
|
}
|
|
|
|
inputNormalized := dataToString(val)
|
|
|
|
for i, entry := range *l {
|
|
if dataToString(entry.Value) == inputNormalized {
|
|
(*l)[i].Comment = comment
|
|
(*l)[i].IsNegative = isNegative
|
|
return nil
|
|
}
|
|
}
|
|
|
|
*l = append(*l, IPEntry{
|
|
Value: val,
|
|
Comment: comment,
|
|
IsNegative: isNegative,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// Contains checks if the specified net.IP matches any rule in the list.
|
|
// It respects the order of entries and correctly handles negative (exclusion) rules.
|
|
//
|
|
// Parameters:
|
|
// - ip: the net.IP address to check.
|
|
//
|
|
// Returns:
|
|
// - bool: true if the IP is allowed by the list rules, false otherwise.
|
|
func (l IPList) Contains(ip net.IP) bool {
|
|
if ip == nil {
|
|
return false
|
|
}
|
|
matched := false
|
|
for _, entry := range l {
|
|
if matchAny(entry.Value, ip) {
|
|
if entry.IsNegative {
|
|
matched = false
|
|
} else {
|
|
matched = true
|
|
}
|
|
}
|
|
}
|
|
return matched
|
|
}
|
|
|
|
// Delete removes an entry from the IP list matching the provided input string.
|
|
//
|
|
// Parameters:
|
|
// - input: the IP address, CIDR, or range to remove. The "!" prefix is ignored during matching.
|
|
//
|
|
// Returns:
|
|
// - error: an error if the entry is not found or the format is invalid.
|
|
func (l *IPList) Delete(input string) error {
|
|
cleanInput := strings.TrimPrefix(strings.TrimSpace(input), "!")
|
|
val := parseDataPart(cleanInput)
|
|
|
|
if _, ok := val.(IPUnknown); ok {
|
|
return fmt.Errorf("invalid IP format: %s", input)
|
|
}
|
|
|
|
inputNormalized := dataToString(val)
|
|
|
|
for i, entry := range *l {
|
|
if dataToString(entry.Value) == inputNormalized {
|
|
*l = append((*l)[:i], (*l)[i+1:]...)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("entry not found: %s", input)
|
|
}
|
|
|
|
// GetEntry retrieves a pointer to an IPEntry matching the input string.
|
|
//
|
|
// Parameters:
|
|
// - input: the IP representation to search for.
|
|
//
|
|
// Returns:
|
|
// - *[IPEntry]: a pointer to the entry if found, nil otherwise.
|
|
func (l IPList) GetEntry(input string) *IPEntry {
|
|
idx := l.indexOf(input)
|
|
if idx == -1 {
|
|
return nil
|
|
}
|
|
return &l[idx]
|
|
}
|
|
|
|
// Insert places a new entry at a specific index in the list.
|
|
//
|
|
// Parameters:
|
|
// - index: the position at which to insert the new entry.
|
|
// - input: the IP rule string (supports "!" prefix).
|
|
// - comment: the comment for the entry.
|
|
// - alignPos: the visual position of the semicolon for comment alignment.
|
|
//
|
|
// Returns:
|
|
// - error: an error if the input format is invalid.
|
|
func (l *IPList) Insert(index int, input string, comment string, alignPos int) error {
|
|
isNegative := false
|
|
trimmedInput := strings.TrimSpace(input)
|
|
|
|
if strings.HasPrefix(trimmedInput, "!") {
|
|
isNegative = true
|
|
trimmedInput = strings.TrimSpace(trimmedInput[1:])
|
|
}
|
|
|
|
val := parseDataPart(trimmedInput)
|
|
if _, ok := val.(IPUnknown); ok {
|
|
return fmt.Errorf("invalid IP format: %s", input)
|
|
}
|
|
|
|
_ = l.Delete(trimmedInput)
|
|
|
|
newEntry := IPEntry{
|
|
Value: val,
|
|
Comment: comment,
|
|
CommentPos: alignPos,
|
|
IsNegative: isNegative,
|
|
}
|
|
|
|
if index < 0 {
|
|
index = 0
|
|
}
|
|
if index > len(*l) {
|
|
index = len(*l)
|
|
}
|
|
|
|
*l = append((*l)[:index], append([]IPEntry{newEntry}, (*l)[index:]...)...)
|
|
return nil
|
|
}
|
|
|
|
// InsertAfter places a new entry immediately following a specific marker entry.
|
|
//
|
|
// Parameters:
|
|
// - marker: the existing entry value to look for.
|
|
// - input: the new IP rule string to insert.
|
|
// - comment: the comment for the new entry.
|
|
// - alignPos: the visual position of the semicolon for comment alignment.
|
|
//
|
|
// Returns:
|
|
// - error: an error if the marker is not found or input is invalid.
|
|
func (l *IPList) InsertAfter(marker string, input string, comment string, alignPos int) error {
|
|
idx := l.indexOf(marker)
|
|
if idx == -1 {
|
|
return fmt.Errorf("marker not found: %s", marker)
|
|
}
|
|
return l.Insert(idx+1, input, comment, alignPos)
|
|
}
|
|
|
|
// InsertBefore places a new entry immediately preceding a specific marker entry.
|
|
//
|
|
// Parameters:
|
|
// - marker: the existing entry value to look for.
|
|
// - input: the new IP rule string to insert.
|
|
// - comment: the comment for the new entry.
|
|
// - alignPos: the visual position of the semicolon for comment alignment.
|
|
//
|
|
// Returns:
|
|
// - error: an error if the marker is not found or input is invalid.
|
|
func (l *IPList) InsertBefore(marker string, input string, comment string, alignPos int) error {
|
|
idx := l.indexOf(marker)
|
|
if idx == -1 {
|
|
return fmt.Errorf("marker not found: %s", marker)
|
|
}
|
|
return l.Insert(idx, input, comment, alignPos)
|
|
}
|
|
|
|
func dataToString(val any) string {
|
|
switch v := val.(type) {
|
|
case IPCIDR:
|
|
ones, bits := v.Mask.Size()
|
|
if ones == bits {
|
|
return v.IP.String()
|
|
}
|
|
return v.IP.String() + "/" + strconv.Itoa(ones)
|
|
case IPRange:
|
|
return v.Start.String() + "-" + v.End.String()
|
|
case IPSeq:
|
|
parts := make([]string, len(v))
|
|
for i, sub := range v {
|
|
parts[i] = dataToString(sub)
|
|
}
|
|
return strings.Join(parts, ",")
|
|
case IPUnknown:
|
|
return string(v)
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func (l IPList) indexOf(input string) int {
|
|
cleanInput := strings.TrimPrefix(strings.TrimSpace(input), "!")
|
|
val := parseDataPart(cleanInput)
|
|
|
|
if targetCIDR, ok := val.(IPCIDR); ok {
|
|
for i, entry := range l {
|
|
if matchAny(entry.Value, targetCIDR.IP) {
|
|
return i
|
|
}
|
|
}
|
|
}
|
|
|
|
clean := dataToString(val)
|
|
for i, entry := range l {
|
|
if dataToString(entry.Value) == clean {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func matchAny(val any, ip net.IP) bool {
|
|
switch v := val.(type) {
|
|
case IPCIDR:
|
|
return v.Contains(ip)
|
|
case IPRange:
|
|
return bytes.Compare(ip, v.Start) >= 0 && bytes.Compare(ip, v.End) <= 0
|
|
case IPSeq:
|
|
for _, sub := range v {
|
|
if matchAny(sub, ip) {
|
|
return true
|
|
}
|
|
}
|
|
case IPUnknown:
|
|
if target := net.ParseIP(string(v)); target != nil {
|
|
return target.Equal(ip)
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func parseDataPart(s string) any {
|
|
s = strings.TrimSpace(s)
|
|
if strings.Contains(s, ",") {
|
|
parts := strings.Split(s, ",")
|
|
seq := make(IPSeq, 0, len(parts))
|
|
for _, p := range parts {
|
|
seq = append(seq, parseDataPart(p))
|
|
}
|
|
return seq
|
|
}
|
|
|
|
if idx := strings.Index(s, "-"); idx != -1 {
|
|
start := net.ParseIP(strings.TrimSpace(s[:idx]))
|
|
end := net.ParseIP(strings.TrimSpace(s[idx+1:]))
|
|
if start != nil && end != nil {
|
|
return IPRange{Start: start, End: end}
|
|
}
|
|
}
|
|
|
|
if !strings.Contains(s, "/") {
|
|
if ip := net.ParseIP(s); ip != nil {
|
|
if ip4 := ip.To4(); ip4 != nil {
|
|
ip = ip4
|
|
}
|
|
mask := net.CIDRMask(len(ip)*8, len(ip)*8)
|
|
return IPCIDR{net.IPNet{IP: ip, Mask: mask}}
|
|
}
|
|
} else if _, ipnet, err := net.ParseCIDR(s); err == nil {
|
|
return IPCIDR{*ipnet}
|
|
}
|
|
|
|
return IPUnknown(s)
|
|
}
|
|
|
|
func parseIPList(raw string) IPList {
|
|
lines := strings.Split(raw, "\n")
|
|
list := make(IPList, 0, len(lines))
|
|
|
|
for _, line := range lines {
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
entry := IPEntry{}
|
|
idx := strings.IndexByte(line, ';')
|
|
dataPart := line
|
|
|
|
if idx != -1 {
|
|
entry.CommentPos = idx
|
|
entry.Comment = line[idx+1:]
|
|
dataPart = line[:idx]
|
|
}
|
|
|
|
trimmedData := strings.TrimSpace(dataPart)
|
|
if strings.HasPrefix(trimmedData, "!") {
|
|
entry.IsNegative = true
|
|
trimmedData = strings.TrimSpace(trimmedData[1:])
|
|
}
|
|
|
|
if trimmedData != "" {
|
|
entry.Value = parseDataPart(trimmedData)
|
|
list = append(list, entry)
|
|
} else if entry.Comment != "" {
|
|
list = append(list, entry)
|
|
}
|
|
}
|
|
return list
|
|
}
|
|
|
|
func (l IPList) writeCGP(w encodeWriter) (err error) {
|
|
if err = w.WriteByte('"'); err != nil {
|
|
return
|
|
}
|
|
|
|
for i, entry := range l {
|
|
if i > 0 {
|
|
if _, err = w.WriteString(`\e`); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
if entry.IsNegative {
|
|
if err = w.WriteByte('!'); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
if err = writeDataToString(w, entry.Value); err != nil {
|
|
return
|
|
}
|
|
|
|
if entry.Comment != "" {
|
|
dataStrLen := len(dataToString(entry.Value))
|
|
if entry.IsNegative {
|
|
dataStrLen++
|
|
}
|
|
|
|
if entry.CommentPos > dataStrLen {
|
|
w.WriteString(strings.Repeat(" ", entry.CommentPos-dataStrLen))
|
|
} else {
|
|
w.WriteByte(' ')
|
|
}
|
|
|
|
if err = w.WriteByte(';'); err != nil {
|
|
return
|
|
}
|
|
|
|
if _, err = w.WriteString(replaceSpecChars(entry.Comment)); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
return w.WriteByte('"')
|
|
}
|
|
|
|
// writeDataToString пишет строковое представление IP-данных напрямую в writer
|
|
func writeDataToString(w encodeWriter, val any) error {
|
|
switch v := val.(type) {
|
|
case IPCIDR:
|
|
ones, bits := v.Mask.Size()
|
|
w.WriteString(v.IP.String())
|
|
if ones != bits {
|
|
w.WriteByte('/')
|
|
w.WriteString(strconv.Itoa(ones))
|
|
}
|
|
case IPRange:
|
|
w.WriteString(v.Start.String())
|
|
w.WriteByte('-')
|
|
w.WriteString(v.End.String())
|
|
case IPSeq:
|
|
for i, sub := range v {
|
|
if i > 0 {
|
|
w.WriteByte(',')
|
|
}
|
|
if err := writeDataToString(w, sub); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
case IPUnknown:
|
|
w.WriteString(string(v))
|
|
}
|
|
return nil
|
|
}
|