Files
Mikrotik-arpwatch/arpwatch_with_bridge_hosts.rsc
T
Evgeny 264f05d503 first
2025-12-15 06:13:59 +03:00

173 lines
5.8 KiB
Plaintext

# ARPWATCH — simple & reliable version with per-MAC files in "arpwatch" folder
:local folder "arpwatch"
:local dbFile ($folder . "/arpwatch.csv")
:local sendTo "svetloe@koronovonoe-pivo.ru"
:local emailSubjectPrefix "ARPWatch"
:local date [/system clock get date]
:local time [/system clock get time]
:local now ($date . " " . $time)
:log info ("arpwatch: starting run at " . $now)
# Ensure folder exists
:if ([:len [/file find name=$folder]] = 0) do={
/file make-directory $folder
:log info ("arpwatch: created folder " . $folder)
}
# Ensure CSV exists
:if ([:len [/file find name=$dbFile]] = 0) do={
/file add name=$dbFile contents="mac,ip,first_seen,vendor"
:log info ("arpwatch: created database file " . $dbFile)
}
# Read current CSV for in-memory append
:local oldCSV [/file get [/file find name=$dbFile] contents]
# Buffers/counters
:local newRows ""
:local newCount 0
:local arpCount 0
# Count ARP entries
:foreach id in=[/ip arp find] do={ :set arpCount ($arpCount + 1) }
:log info ("arpwatch: ARP entries found: " . $arpCount)
############################
# Iterate through ARP table
############################
:foreach id in=[/ip arp find] do={
:local mac [/ip arp get $id mac-address]
:local ip [/ip arp get $id address]
:local iface [/ip arp get $id interface]
# Skip empty
:if ([:len $mac] = 0) do={
:log info ("arpwatch: skipping, empty MAC (iface=" . $iface . " ip=" . $ip . ")")
} else={
:if ([:len $ip] = 0) do={
:log info ("arpwatch: skipping, empty IP for MAC " . $mac)
} else={
# Normalize MAC and filename
:local macFileCore ""
:local i 0
:local maclen [:len $mac]
:for i from=0 to=($maclen - 1) do={
:local ch [:pick $mac $i ($i + 1)]
:if ($ch = ":") do={ :set macFileCore ($macFileCore . "-") } else={ :set macFileCore ($macFileCore . $ch) }
}
:local macFile ($folder . "/mac-" . $macFileCore)
:log info ("arpwatch: checking MAC " . $mac . " at " . $ip . " on " . $iface)
# Check if exists
:if ([:len [/file find name=$macFile]] > 0) do={
:log info ("arpwatch: MAC exists, skip: " . $mac)
} else={
:log info ("arpwatch: NEW MAC detected: " . $mac)
:local row ($mac . "," . $ip . "," . $now . ",https://maclookup.app/search/result?mac=" . $mac)
:if ([:len $newRows] = 0) do={ :set newRows $row } else={ :set newRows ($newRows . "\n" . $row) }
:set oldCSV ($oldCSV . "\n" . $row)
/file add name=$macFile contents=$now
:set newCount ($newCount + 1)
}
}
}
}
:log info ("arpwatch: new MAC count from ARP = " . $newCount)
##########################################
# Also check bridge host table (bridge hosts)
# Use 'print as-value' and -> operator to safely access fields
##########################################
:local bridgeCount 0
:foreach item in=[/interface bridge host print as-value] do={
# access properties from associative array
:local bmac ($item->"mac-address")
:local biface ($item->"on-interface")
:local bvlan ($item->"vlan-id")
:if ([:len $bmac] = 0) do={
:log info ("arpwatch: bridge host entry with empty MAC, iface=" . $biface)
} else={
# Try to find IP for this MAC from ARP table (if present)
:local ip ""
:local arpMatch [/ip arp find where mac-address=$bmac]
:if ([:len $arpMatch] > 0) do={
:set ip [/ip arp get $arpMatch address]
} else={
:set ip ""
}
# Normalize MAC and filename (same as above)
:local macFileCore ""
:local i 0
:local maclen [:len $bmac]
:for i from=0 to=($maclen - 1) do={
:local ch [:pick $bmac $i ($i + 1)]
:if ($ch = ":") do={ :set macFileCore ($macFileCore . "-") } else={ :set macFileCore ($macFileCore . $ch) }
}
:local macFile ($folder . "/mac-" . $macFileCore)
:log info ("arpwatch: checking BRIDGE MAC " . $bmac . " on " . $biface . " vlan=" . $bvlan)
# If per-MAC file already exists, skip (this prevents duplicates if ARP already handled it)
:if ([:len [/file find name=$macFile]] > 0) do={
:log info ("arpwatch: BRIDGE MAC exists, skip: " . $bmac)
} else={
:log info ("arpwatch: NEW BRIDGE MAC detected: " . $bmac)
# Use IP if found, otherwise leave empty field
:local row ($bmac . "," . $ip . "," . $now . ",https://maclookup.app/search/result?mac=" . $bmac)
:if ([:len $newRows] = 0) do={ :set newRows $row } else={ :set newRows ($newRows . "\n" . $row) }
:set oldCSV ($oldCSV . "\n" . $row)
/file add name=$macFile contents=$now
:set newCount ($newCount + 1)
:set bridgeCount ($bridgeCount + 1)
}
}
}
:log info ("arpwatch: new MAC count from BRIDGE = " . $bridgeCount)
:log info ("arpwatch: total new MAC count = " . $newCount)
# Update CSV and send mail
:if ($newCount > 0) do={
:do {
/file set [/file find name=$dbFile] contents=$oldCSV
:log info ("arpwatch: database updated, appended " . $newCount . " new row(s)")
} on-error={
:log error "arpwatch: failed to update CSV"
}
:local subject ($emailSubjectPrefix . " - " . $newCount . " new MAC(s)")
:local body ("New MAC addresses detected at " . $now . ":\n\n" . $newRows . "\n\nDatabase: " . $dbFile)
:do {
/tool e-mail send to=$sendTo subject=$subject body=$body
:log info ("arpwatch: e-mail sent to " . $sendTo)
} on-error={
:log error "arpwatch: failed to send e-mail"
}
} else={
:log info "arpwatch: no new MACs found, nothing to update or send"
}
:log info "arpwatch: run finished"