Files
rspamd-cgp/cgp/hostname.go
T
ai 4cf590ef5f perf!: streaming I/O refactoring, full test coverage, and v3.0.0
- Implemented streaming processing for constant memory footprint.
- Optimized memory usage: reduced allocations from 185 to 99.
- Migrated `config.Direction` to typed Enum.
- Added comprehensive test suite for config, cgp, rspamc and internal logic.
- Cleaned up loop protection and action handlers.
2026-03-05 23:19:40 +03:00

207 lines
4.4 KiB
Go

package cgp
import (
"context"
"net"
"net/netip"
"strings"
"time"
"github.com/maypok86/otter"
)
func filterNames(names []string) []string {
fqdn := make([]string, 0, len(names))
nonfqdn := make([]string, 0, len(names))
for _, name := range names {
if IsValidDomain(name) {
fqdn = append(fqdn, name)
} else {
nonfqdn = append(nonfqdn, name)
}
}
if len(fqdn) > 0 {
return fqdn
} else {
return nonfqdn
}
}
func findLongerItem(items []string) (res string) {
if len(items) > 0 {
var maxlen = 0
var pos int
for p, item := range items {
if len(item) > maxlen {
maxlen = len(item)
pos = p
}
}
res = items[pos]
}
return
}
func findShorterItem(items []string) (res string) {
if len(items) > 0 {
var maxlen = len(items[0])
var pos int
for p, item := range items {
if len(item) < maxlen {
maxlen = len(item)
pos = p
}
}
res = items[pos]
}
return
}
var getHostname = func() func(addr string) (hostname string) {
cache, _ := otter.MustBuilder[netip.Addr, string](1000).
CollectStats().
Cost(func(key netip.Addr, value string) uint32 {
return 1
}).
WithVariableTTL().
Build()
return func(addr string) (hostname string) {
ipAddr, err := netip.ParseAddr(addr)
if err != nil {
return
}
hostname, ok := cache.Get(ipAddr)
if !ok {
hostname = lookupAddr(ipAddr)
if len(hostname) > 0 {
cache.Set(ipAddr, hostname, time.Hour)
} else {
cache.Set(ipAddr, hostname, 5*time.Minute)
}
}
return
}
}()
func IsValidDomain(domain string) bool {
if len(domain) == 0 || len(domain) > 253 {
return false
}
// Fast path: IP-адреса (v4 и v6) обычно начинаются с цифры или двоеточия.
// Домены по RFC могут начинаться с цифры, но это редкость.
// Если первый символ - цифра или ':', проверяем, не IP ли это.
first := domain[0]
if (first >= '0' && first <= '9') || first == ':' {
if _, err := netip.ParseAddr(strings.TrimSuffix(domain, ".")); err == nil {
return false // Это чистый IP
}
}
domain = strings.TrimSuffix(domain, ".")
lastDot := -1
hasDot := false
for i := 0; i < len(domain); i++ {
c := domain[i]
if c == '.' {
labelLen := i - lastDot - 1
// Пустые метки (..) или дефис по краям метки недопустимы
if labelLen == 0 || labelLen > 63 || domain[lastDot+1] == '-' || domain[i-1] == '-' {
return false
}
lastDot = i
hasDot = true
continue
}
// Допустимые символы: [a-zA-Z0-9_-]
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_') {
return false
}
}
// Проверка последней метки (TLD)
finalLabelLen := len(domain) - lastDot - 1
if finalLabelLen == 0 || finalLabelLen > 63 || domain[lastDot+1] == '-' || domain[len(domain)-1] == '-' {
return false
}
return hasDot
}
func lookupAddr(ipAddr netip.Addr) (hostname string) {
r := &net.Resolver{PreferGo: true}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
names, err := r.LookupAddr(ctx, ipAddr.String())
if err != nil {
return
}
if len(names) == 1 {
hostname = strings.TrimSuffix(names[0], ".")
return
}
type result struct{ name string }
ch := make(chan result, len(names))
for _, name := range names {
go func(name string) {
ips, err := r.LookupHost(ctx, name)
if err != nil {
ch <- result{}
return
}
for _, ip := range ips {
ipTest, err := netip.ParseAddr(ip)
if err != nil {
continue
}
if ipTest == ipAddr {
ch <- result{name}
return
}
}
ch <- result{}
}(name)
}
found := make([]string, 0, len(names))
for range names {
if r := <-ch; r.name != "" {
found = append(found, r.name)
}
}
switch len(found) {
case 0:
// если ни один name не имеет корректного прямого резольвинга
// в исходный ip, выбираем самый длинный.
hostname = findLongerItem(filterNames(names))
case 1:
hostname = found[0]
default:
// если несколько name имеют корректный прямой резольвинг
// в исходный ip, выбираем самый короткий.
hostname = findShorterItem(filterNames(found))
}
hostname = strings.TrimSuffix(hostname, ".")
return
}