fix(snmp,db): save FDB to PG (empty inet null), bridge_port + MAC normalize

Made-with: Cursor
This commit is contained in:
PTah
2026-04-13 11:58:48 +10:00
parent 264865f9d8
commit 936cc6b9bb
6 changed files with 108 additions and 27 deletions
+14 -6
View File
@@ -6,6 +6,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"sort" "sort"
"strings"
"time" "time"
) )
@@ -541,16 +542,23 @@ func (s *PostgresStore) PurgeAllScanData() error {
func (s *PostgresStore) SavePortDeviceResult(scanID string, result PortDeviceResult) error { func (s *PostgresStore) SavePortDeviceResult(scanID string, result PortDeviceResult) error {
query := ` query := `
insert into scan_port_devices (scan_id, ip, if_index, mac, learned_ip, checked_at) insert into scan_port_devices (scan_id, ip, if_index, bridge_port, mac, learned_ip, checked_at)
values ($1, $2, $3, $4, $5, $6)` values ($1, $2, $3, $4, $5, $6, $7)`
var learnedIP any
if v := strings.TrimSpace(result.LearnedIP); v != "" {
learnedIP = v
} else {
learnedIP = nil
}
_, err := s.db.ExecContext( _, err := s.db.ExecContext(
context.Background(), context.Background(),
query, query,
scanID, scanID,
result.IP, result.IP,
result.IfIndex, result.IfIndex,
result.BridgePort,
result.MAC, result.MAC,
result.LearnedIP, learnedIP,
result.CheckedAt, result.CheckedAt,
) )
return err return err
@@ -561,11 +569,11 @@ func (s *PostgresStore) ListPortDeviceResults(scanID string, filterIP string, fi
limit = 20000 limit = 20000
} }
query := ` query := `
select ip::text, if_index, coalesce(mac, ''), coalesce(learned_ip::text, ''), checked_at select ip::text, if_index, bridge_port, coalesce(mac, ''), coalesce(learned_ip::text, ''), checked_at
from scan_port_devices from scan_port_devices
where scan_id = $1 where scan_id = $1
and ($2 = '' or ip = $2::inet) and ($2 = '' or ip = $2::inet)
and ($3 = 0 or if_index = $3) and ($3 = 0 or if_index = $3 or bridge_port = $3)
order by if_index asc, mac asc, learned_ip asc order by if_index asc, mac asc, learned_ip asc
limit $4` limit $4`
rows, err := s.db.QueryContext(context.Background(), query, scanID, filterIP, filterIfIndex, limit) rows, err := s.db.QueryContext(context.Background(), query, scanID, filterIP, filterIfIndex, limit)
@@ -577,7 +585,7 @@ limit $4`
out := make([]PortDeviceResult, 0, 256) out := make([]PortDeviceResult, 0, 256)
for rows.Next() { for rows.Next() {
var r PortDeviceResult var r PortDeviceResult
if err = rows.Scan(&r.IP, &r.IfIndex, &r.MAC, &r.LearnedIP, &r.CheckedAt); err != nil { if err = rows.Scan(&r.IP, &r.IfIndex, &r.BridgePort, &r.MAC, &r.LearnedIP, &r.CheckedAt); err != nil {
return nil return nil
} }
out = append(out, r) out = append(out, r)
+63 -5
View File
@@ -245,8 +245,8 @@ func probePortDevices(client *gosnmp.GoSNMP, ip string, checkedAt time.Time) []P
macToIPs := make(map[string]map[string]struct{}) macToIPs := make(map[string]map[string]struct{})
for key, mac := range arpIfIPToMac { for key, mac := range arpIfIPToMac {
mac = strings.ToLower(strings.TrimSpace(mac)) mac = normalizeMACKey(mac)
if mac == "" || !strings.Contains(mac, ":") { if mac == "" {
continue continue
} }
parts := strings.Split(key, ".") parts := strings.Split(key, ".")
@@ -266,11 +266,11 @@ func probePortDevices(client *gosnmp.GoSNMP, ip string, checkedAt time.Time) []P
seen := make(map[string]struct{}) seen := make(map[string]struct{})
out := make([]PortDeviceResult, 0, len(combinedFdb)) out := make([]PortDeviceResult, 0, len(combinedFdb))
for macTail, bridgePortRaw := range combinedFdb { for macTail, bridgePortRaw := range combinedFdb {
mac := dottedDecimalToMACTail(macTail) mac := normalizeMACKey(dottedDecimalToMACTail(macTail))
if mac == "" { if mac == "" {
continue continue
} }
bridgePort, err := strconv.Atoi(strings.TrimSpace(bridgePortRaw)) bridgePort, err := strconv.Atoi(strings.TrimSpace(snmpNumericString(bridgePortRaw)))
if err != nil || bridgePort <= 0 { if err != nil || bridgePort <= 0 {
continue continue
} }
@@ -283,7 +283,7 @@ func probePortDevices(client *gosnmp.GoSNMP, ip string, checkedAt time.Time) []P
continue continue
} }
ipsSet := macToIPs[strings.ToLower(mac)] ipsSet := macToIPs[mac]
if len(ipsSet) == 0 { if len(ipsSet) == 0 {
key := fmt.Sprintf("%d|%s|", ifIndex, mac) key := fmt.Sprintf("%d|%s|", ifIndex, mac)
if _, ok := seen[key]; ok { if _, ok := seen[key]; ok {
@@ -293,6 +293,7 @@ func probePortDevices(client *gosnmp.GoSNMP, ip string, checkedAt time.Time) []P
out = append(out, PortDeviceResult{ out = append(out, PortDeviceResult{
IP: ip, IP: ip,
IfIndex: ifIndex, IfIndex: ifIndex,
BridgePort: bridgePort,
MAC: mac, MAC: mac,
LearnedIP: "", LearnedIP: "",
CheckedAt: checkedAt, CheckedAt: checkedAt,
@@ -308,6 +309,7 @@ func probePortDevices(client *gosnmp.GoSNMP, ip string, checkedAt time.Time) []P
out = append(out, PortDeviceResult{ out = append(out, PortDeviceResult{
IP: ip, IP: ip,
IfIndex: ifIndex, IfIndex: ifIndex,
BridgePort: bridgePort,
MAC: mac, MAC: mac,
LearnedIP: learnedIP, LearnedIP: learnedIP,
CheckedAt: checkedAt, CheckedAt: checkedAt,
@@ -331,6 +333,62 @@ func probePortDevices(client *gosnmp.GoSNMP, ip string, checkedAt time.Time) []P
return out return out
} }
// snmpNumericString — значение SNMP как строка (int/float/[]byte) для Atoi.
func snmpNumericString(v any) string {
switch x := v.(type) {
case string:
return x
case []byte:
return strings.TrimSpace(string(x))
default:
return strings.TrimSpace(fmt.Sprint(x))
}
}
// normalizeMACKey приводит MAC к виду aa:bb:cc:dd:ee:ff для сопоставления FDB и ARP.
func normalizeMACKey(s string) string {
s = strings.ToLower(strings.TrimSpace(s))
if s == "" {
return ""
}
s = strings.ReplaceAll(s, "-", ":")
if strings.Count(s, ":") == 5 {
parts := strings.Split(s, ":")
if len(parts) != 6 {
return ""
}
var b [6]byte
for i, p := range parts {
n, err := strconv.ParseUint(strings.TrimSpace(p), 16, 8)
if err != nil {
return ""
}
b[i] = byte(n)
}
return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", b[0], b[1], b[2], b[3], b[4], b[5])
}
if strings.HasPrefix(s, "0x") {
h := strings.TrimPrefix(s, "0x")
h = strings.ReplaceAll(h, ":", "")
if len(h) != 12 {
return ""
}
raw, err := hex.DecodeString(h)
if err != nil || len(raw) != 6 {
return ""
}
return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", raw[0], raw[1], raw[2], raw[3], raw[4], raw[5])
}
if len(s) == 12 {
raw, err := hex.DecodeString(s)
if err != nil || len(raw) != 6 {
return ""
}
return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", raw[0], raw[1], raw[2], raw[3], raw[4], raw[5])
}
return ""
}
func dottedDecimalToMACTail(s string) string { func dottedDecimalToMACTail(s string) string {
p := strings.Split(strings.TrimSpace(s), ".") p := strings.Split(strings.TrimSpace(s), ".")
if len(p) < 6 { if len(p) < 6 {
+2 -1
View File
@@ -121,6 +121,7 @@ type InterfaceResult struct {
type PortDeviceResult struct { type PortDeviceResult struct {
IP string `json:"ip"` IP string `json:"ip"`
IfIndex int `json:"if_index"` IfIndex int `json:"if_index"`
BridgePort int `json:"bridge_port,omitempty"`
MAC string `json:"mac"` MAC string `json:"mac"`
LearnedIP string `json:"learned_ip"` LearnedIP string `json:"learned_ip"`
CheckedAt time.Time `json:"checked_at"` CheckedAt time.Time `json:"checked_at"`
@@ -409,7 +410,7 @@ func (s *MemoryStore) ListPortDeviceResults(scanID string, filterIP string, filt
if filterIP != "" && r.IP != filterIP { if filterIP != "" && r.IP != filterIP {
continue continue
} }
if filterIfIndex > 0 && r.IfIndex != filterIfIndex { if filterIfIndex > 0 && r.IfIndex != filterIfIndex && r.BridgePort != filterIfIndex {
continue continue
} }
filtered = append(filtered, r) filtered = append(filtered, r)
+6
View File
@@ -14,6 +14,9 @@ var scanInterfacesSQL string
//go:embed sql/003_scan_port_devices.sql //go:embed sql/003_scan_port_devices.sql
var scanPortDevicesSQL string var scanPortDevicesSQL string
//go:embed sql/004_scan_port_devices_bridge_port.sql
var scanPortDevicesBridgePortSQL string
func RunMigrations(db *sql.DB) error { func RunMigrations(db *sql.DB) error {
if _, err := db.Exec(initSQL); err != nil { if _, err := db.Exec(initSQL); err != nil {
return err return err
@@ -24,5 +27,8 @@ func RunMigrations(db *sql.DB) error {
if _, err := db.Exec(scanPortDevicesSQL); err != nil { if _, err := db.Exec(scanPortDevicesSQL); err != nil {
return err return err
} }
if _, err := db.Exec(scanPortDevicesBridgePortSQL); err != nil {
return err
}
return nil return nil
} }
@@ -3,6 +3,7 @@ create table if not exists scan_port_devices (
scan_id text not null references scan_jobs(id) on delete cascade, scan_id text not null references scan_jobs(id) on delete cascade,
ip inet not null, ip inet not null,
if_index int not null, if_index int not null,
bridge_port int not null default 0,
mac text not null, mac text not null,
learned_ip inet null, learned_ip inet null,
checked_at timestamptz not null checked_at timestamptz not null
@@ -13,3 +14,6 @@ create index if not exists idx_scan_port_devices_scan_ip_if
create index if not exists idx_scan_port_devices_scan_ip_mac create index if not exists idx_scan_port_devices_scan_ip_mac
on scan_port_devices (scan_id, ip, mac); on scan_port_devices (scan_id, ip, mac);
create index if not exists idx_scan_port_devices_scan_ip_br
on scan_port_devices (scan_id, ip, bridge_port);
@@ -0,0 +1,4 @@
alter table scan_port_devices add column if not exists bridge_port int not null default 0;
create index if not exists idx_scan_port_devices_scan_ip_br
on scan_port_devices (scan_id, ip, bridge_port);