fix(snmp,db): save FDB to PG (empty inet null), bridge_port + MAC normalize
Made-with: Cursor
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
+73
-15
@@ -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 {
|
||||||
@@ -291,11 +291,12 @@ func probePortDevices(client *gosnmp.GoSNMP, ip string, checkedAt time.Time) []P
|
|||||||
}
|
}
|
||||||
seen[key] = struct{}{}
|
seen[key] = struct{}{}
|
||||||
out = append(out, PortDeviceResult{
|
out = append(out, PortDeviceResult{
|
||||||
IP: ip,
|
IP: ip,
|
||||||
IfIndex: ifIndex,
|
IfIndex: ifIndex,
|
||||||
MAC: mac,
|
BridgePort: bridgePort,
|
||||||
LearnedIP: "",
|
MAC: mac,
|
||||||
CheckedAt: checkedAt,
|
LearnedIP: "",
|
||||||
|
CheckedAt: checkedAt,
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -306,11 +307,12 @@ func probePortDevices(client *gosnmp.GoSNMP, ip string, checkedAt time.Time) []P
|
|||||||
}
|
}
|
||||||
seen[key] = struct{}{}
|
seen[key] = struct{}{}
|
||||||
out = append(out, PortDeviceResult{
|
out = append(out, PortDeviceResult{
|
||||||
IP: ip,
|
IP: ip,
|
||||||
IfIndex: ifIndex,
|
IfIndex: ifIndex,
|
||||||
MAC: mac,
|
BridgePort: bridgePort,
|
||||||
LearnedIP: learnedIP,
|
MAC: mac,
|
||||||
CheckedAt: checkedAt,
|
LearnedIP: learnedIP,
|
||||||
|
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 {
|
||||||
|
|||||||
@@ -119,11 +119,12 @@ type InterfaceResult struct {
|
|||||||
|
|
||||||
// PortDeviceResult — MAC/IP, обнаруженные за конкретным портом (ifIndex) через BRIDGE-MIB + ARP.
|
// PortDeviceResult — MAC/IP, обнаруженные за конкретным портом (ifIndex) через BRIDGE-MIB + ARP.
|
||||||
type PortDeviceResult struct {
|
type PortDeviceResult struct {
|
||||||
IP string `json:"ip"`
|
IP string `json:"ip"`
|
||||||
IfIndex int `json:"if_index"`
|
IfIndex int `json:"if_index"`
|
||||||
MAC string `json:"mac"`
|
BridgePort int `json:"bridge_port,omitempty"`
|
||||||
LearnedIP string `json:"learned_ip"`
|
MAC string `json:"mac"`
|
||||||
CheckedAt time.Time `json:"checked_at"`
|
LearnedIP string `json:"learned_ip"`
|
||||||
|
CheckedAt time.Time `json:"checked_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MemoryStore struct {
|
type MemoryStore struct {
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
Reference in New Issue
Block a user