fix
This commit is contained in:
@@ -1,84 +1,118 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#
|
#
|
||||||
# detect_bridge.sh
|
# detect_bridge.sh -- improved / robust argument handling
|
||||||
#
|
#
|
||||||
# Detect whether there is a switch / Linux bridge between THIS host and a target host.
|
# Usage: sudo ./detect_bridge.sh <target-ip-or-hostname> [interface] [--mac-spoof]
|
||||||
# - Usage: sudo ./detect_bridge.sh <target-ip-or-hostname> [interface]
|
|
||||||
# - Optional flags:
|
|
||||||
# --mac-spoof (runs an optional MAC-change test; disabled by default)
|
|
||||||
#
|
#
|
||||||
# Requires: bash, ip, ping, tcpdump, ethtool, arping, awk, grep, sed, date, awk, timeout
|
|
||||||
# lldpctl if available (script will check).
|
|
||||||
#
|
|
||||||
# Notes:
|
|
||||||
# - Many methods require root (tcpdump, ethtool, arping). Run with sudo.
|
|
||||||
# - The script attempts safe tests first. MAC spoofing is optional and may disrupt traffic.
|
|
||||||
# - The script prints a summary and confidence estimate at the end.
|
|
||||||
#
|
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
IFS=$'\n\t'
|
IFS=$'\n\t'
|
||||||
|
|
||||||
TARGET="$1"
|
# defaults
|
||||||
IFACE="${2:-}"
|
TARGET=""
|
||||||
|
IFACE=""
|
||||||
MAC_SPOOF=false
|
MAC_SPOOF=false
|
||||||
|
|
||||||
# parse optional flags
|
# --- parse args robustly ---
|
||||||
for arg in "$@"; do
|
while [[ $# -gt 0 ]]; do
|
||||||
if [ "$arg" = "--mac-spoof" ]; then
|
case "$1" in
|
||||||
MAC_SPOOF=true
|
--mac-spoof)
|
||||||
fi
|
MAC_SPOOF=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-i|--iface)
|
||||||
|
if [[ -n "${2:-}" ]]; then
|
||||||
|
IFACE="$2"
|
||||||
|
shift 2
|
||||||
|
else
|
||||||
|
echo "Error: --iface requires an argument" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
cat <<EOF
|
||||||
|
Usage: sudo $0 <target-ip-or-hostname> [interface] [--mac-spoof]
|
||||||
|
Options:
|
||||||
|
--mac-spoof Enable optional MAC-spoof learning probe (may disrupt link).
|
||||||
|
-i, --iface Specify interface to use (otherwise auto-detected).
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
-*)
|
||||||
|
echo "Unknown option: $1" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if [[ -z "$TARGET" ]]; then
|
||||||
|
TARGET="$1"
|
||||||
|
else
|
||||||
|
# allow second positional to be iface for compatibility
|
||||||
|
if [[ -z "$IFACE" ]]; then
|
||||||
|
IFACE="$1"
|
||||||
|
else
|
||||||
|
echo "Ignoring extra argument: $1" >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
if [[ -z "$TARGET" ]]; then
|
||||||
|
echo "Error: target IP or hostname required." >&2
|
||||||
|
echo "Usage: sudo $0 <target-ip-or-hostname> [interface] [--mac-spoof]" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
# helper: detect default interface to reach target
|
# helper: detect default interface to reach target
|
||||||
detect_iface() {
|
detect_iface() {
|
||||||
if [ -n "$IFACE" ]; then
|
if [[ -n "$IFACE" ]]; then
|
||||||
echo "$IFACE"
|
printf '%s\n' "$IFACE"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
# Use 'ip route get' to find outgoing interface
|
|
||||||
if route_info=$(ip route get "$TARGET" 2>/dev/null); then
|
if route_info=$(ip route get "$TARGET" 2>/dev/null); then
|
||||||
# route_info often contains "dev <iface>"
|
|
||||||
dev=$(echo "$route_info" | awk '{for(i=1;i<=NF;i++){if($i=="dev"){print $(i+1);exit}}}')
|
dev=$(echo "$route_info" | awk '{for(i=1;i<=NF;i++){if($i=="dev"){print $(i+1);exit}}}')
|
||||||
if [ -n "$dev" ]; then
|
if [[ -n "$dev" ]]; then
|
||||||
echo "$dev"
|
printf '%s\n' "$dev"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
# fallback to first non-loopback up interface
|
# fallback: first non-loopback up interface
|
||||||
for dev in $(ls /sys/class/net); do
|
for dev in $(ls /sys/class/net); do
|
||||||
if [ "$dev" != "lo" ] && [ -f "/sys/class/net/$dev/operstate" ] && grep -q "up" "/sys/class/net/$dev/operstate"; then
|
if [[ "$dev" != "lo" ]] && [[ -f "/sys/class/net/$dev/operstate" ]] && grep -q "up" "/sys/class/net/$dev/operstate"; then
|
||||||
echo "$dev"
|
printf '%s\n' "$dev"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
echo "eth0"
|
printf 'eth0\n'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ensure ip exists
|
||||||
if ! command -v ip >/dev/null 2>&1; then
|
if ! command -v ip >/dev/null 2>&1; then
|
||||||
echo "This script requires 'ip' (iproute2). Aborting." >&2
|
echo "This script requires 'ip' (iproute2). Aborting." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
IFACE=$(detect_iface)
|
IFACE=$(detect_iface)
|
||||||
|
|
||||||
echo "Target: $TARGET"
|
echo "Target: $TARGET"
|
||||||
echo "Interface: $IFACE"
|
echo "Interface: $IFACE"
|
||||||
echo
|
echo
|
||||||
|
|
||||||
# ensure we can resolve target ip
|
# resolve target IP
|
||||||
TARGET_IP=""
|
|
||||||
if [[ "$TARGET" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
if [[ "$TARGET" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
TARGET_IP="$TARGET"
|
TARGET_IP="$TARGET"
|
||||||
else
|
else
|
||||||
if ! TARGET_IP=$(getent hosts "$TARGET" | awk '{print $1}' | head -n1); then
|
TARGET_IP=$(getent hosts "$TARGET" | awk '{print $1}' | head -n1 || true)
|
||||||
|
if [[ -z "$TARGET_IP" ]]; then
|
||||||
echo "Failed to resolve target '$TARGET'." >&2
|
echo "Failed to resolve target '$TARGET'." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Resolved target IP: $TARGET_IP"
|
echo "Resolved target IP: $TARGET_IP"
|
||||||
echo
|
echo
|
||||||
|
|
||||||
# check for required commands and print warnings for optional commands
|
# check required commands
|
||||||
REQ_CMDS=(ip ping awk grep sed date)
|
REQ_CMDS=(ip ping awk grep sed date)
|
||||||
for c in "${REQ_CMDS[@]}"; do
|
for c in "${REQ_CMDS[@]}"; do
|
||||||
if ! command -v "$c" >/dev/null 2>&1; then
|
if ! command -v "$c" >/dev/null 2>&1; then
|
||||||
@@ -101,180 +135,146 @@ if command -v lldpctl >/dev/null 2>&1; then HAS_LLDPC=true; fi
|
|||||||
if command -v traceroute >/dev/null 2>&1; then HAS_TRACEROUTE=true; fi
|
if command -v traceroute >/dev/null 2>&1; then HAS_TRACEROUTE=true; fi
|
||||||
|
|
||||||
echo "Tool availability:"
|
echo "Tool availability:"
|
||||||
echo " tcpdump: $HAS_TCPDUMP"
|
echo " tcpdump: $HAS_TCPDUMP"
|
||||||
echo " ethtool: $HAS_ETHTOOL"
|
echo " ethtool: $HAS_ETHTOOL"
|
||||||
echo " arping: $HAS_ARPING"
|
echo " arping: $HAS_ARPING"
|
||||||
echo " lldpctl: $HAS_LLDPC"
|
echo " lldpctl: $HAS_LLDPC"
|
||||||
echo " traceroute: $HAS_TRACEROUTE"
|
echo " traceroute: $HAS_TRACEROUTE"
|
||||||
echo
|
echo
|
||||||
|
|
||||||
# helper to run a command with a nice header
|
|
||||||
run_header() {
|
run_header() {
|
||||||
echo
|
echo
|
||||||
echo "===== $1 ====="
|
echo "===== $1 ====="
|
||||||
}
|
}
|
||||||
|
|
||||||
# 1) interface->master check (fast & safe)
|
# 1) interface->master check
|
||||||
run_header "Interface master / bridge hint (ip link)"
|
run_header "Interface master / bridge hint (ip link)"
|
||||||
ip link show dev "$IFACE" | sed -n '1,4p'
|
ip link show dev "$IFACE" | sed -n '1,4p' || true
|
||||||
# Look for "master <bridge>" or "brd"
|
master=$(ip link show dev "$IFACE" 2>/dev/null | tr '\n' ' ' | sed 's/.*master \([^ ]*\).*/\1/;t;d' || true)
|
||||||
master=$(ip link show dev "$IFACE" 2>/dev/null | tr '\n' ' ' | sed 's/.*master \([^ ]*\).*/\1/;t;d')
|
if [[ -n "$master" ]]; then
|
||||||
if [ -n "$master" ]; then
|
|
||||||
echo "Interface reports master: $master -> This interface is enslaved to a bridge on this host (local bridge)."
|
echo "Interface reports master: $master -> This interface is enslaved to a bridge on this host (local bridge)."
|
||||||
else
|
else
|
||||||
echo "No 'master' shown; interface is not a local bridge slave (or not reported)."
|
echo "No 'master' shown; interface is not a local bridge slave (or not reported)."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 2) check /sys/class/net/<if>/bridge (exists only when this host has a bridge device)
|
# 2) check /sys/class/net/<if>/bridge
|
||||||
run_header "Check local bridge sysfs"
|
run_header "Check local bridge sysfs"
|
||||||
if [ -d "/sys/class/net/$IFACE/bridge" ]; then
|
if [[ -d "/sys/class/net/$IFACE/bridge" ]]; then
|
||||||
echo "/sys/class/net/$IFACE/bridge exists -> this host has a bridge device attached to $IFACE (local)."
|
echo "/sys/class/net/$IFACE/bridge exists -> this host has a bridge device attached to $IFACE (local)."
|
||||||
else
|
else
|
||||||
echo "No /sys/class/net/$IFACE/bridge -> local host is not the bridge owner for this interface."
|
echo "No /sys/class/net/$IFACE/bridge -> local host is not the bridge owner for this interface."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 3) LLDP via lldpctl (if installed)
|
# 3) LLDP via lldpctl
|
||||||
if $HAS_LLDPC; then
|
if $HAS_LLDPC; then
|
||||||
run_header "LLDP via lldpctl (if lldpd installed) -- shows neighbor(s) when available"
|
run_header "LLDP via lldpctl"
|
||||||
echo "(running: lldpctl -f keyvalue on $IFACE)"
|
|
||||||
sudo lldpctl -f keyvalue "$IFACE" 2>/dev/null || echo "lldpctl produced no output or isn't running for $IFACE."
|
sudo lldpctl -f keyvalue "$IFACE" 2>/dev/null || echo "lldpctl produced no output or isn't running for $IFACE."
|
||||||
else
|
else
|
||||||
run_header "LLDP: lldpctl not installed"
|
run_header "LLDP: lldpctl not installed"
|
||||||
echo "lldpctl not installed. You can install lldpd (Debian/Ubuntu: apt install lldpd) and restart it to try LLDP discovery."
|
echo "lldpctl not installed. Install lldpd and run lldpctl to try LLDP discovery."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 4) passive capture for STP/LLDP (tcpdump) - captures broadcast control frames
|
# 4) passive capture for STP/LLDP (tcpdump)
|
||||||
if $HAS_TCPDUMP; then
|
if $HAS_TCPDUMP; then
|
||||||
run_header "Passive capture: look for STP BPDUs and LLDP frames (tcpdump, 6s capture)"
|
run_header "Passive capture: look for STP BPDUs and LLDP frames (tcpdump, 6s capture)"
|
||||||
echo "Capturing for 6 seconds on $IFACE for STP (01:80:c2:00:00:00) and LLDP (0x88cc)"
|
|
||||||
TMPPCAP=$(mktemp /tmp/detect_bridge_pcap.XXXX.pcap)
|
TMPPCAP=$(mktemp /tmp/detect_bridge_pcap.XXXX.pcap)
|
||||||
|
trap 'rm -f "$TMPPCAP"' EXIT
|
||||||
sudo timeout 6 tcpdump -i "$IFACE" -s 128 -w "$TMPPCAP" "ether dst 01:80:c2:00:00:00 or ether proto 0x88cc or ether multicast" >/dev/null 2>&1 || true
|
sudo timeout 6 tcpdump -i "$IFACE" -s 128 -w "$TMPPCAP" "ether dst 01:80:c2:00:00:00 or ether proto 0x88cc or ether multicast" >/dev/null 2>&1 || true
|
||||||
if [ -s "$TMPPCAP" ]; then
|
if [[ -s "$TMPPCAP" ]]; then
|
||||||
echo "Captured packets into $TMPPCAP. Decoding summary (tcpdump -r):"
|
echo "Captured packets; summary:"
|
||||||
sudo tcpdump -n -r "$TMPPCAP" -e | sed -n '1,50p' || true
|
sudo tcpdump -n -r "$TMPPCAP" -e | sed -n '1,50p' || true
|
||||||
# search for STP and LLDP keywords
|
|
||||||
if sudo tcpdump -n -r "$TMPPCAP" -e | grep -i "stp\|bpdu" >/dev/null 2>&1; then
|
if sudo tcpdump -n -r "$TMPPCAP" -e | grep -i "stp\|bpdu" >/dev/null 2>&1; then
|
||||||
echo "-> STP/BPDU frames observed. Very likely a bridge/switch present (could be Linux bridge running STP)."
|
echo "-> STP/BPDU frames observed."
|
||||||
fi
|
fi
|
||||||
if sudo tcpdump -n -r "$TMPPCAP" -e | grep -i "LLDP" >/dev/null 2>&1; then
|
if sudo tcpdump -n -r "$TMPPCAP" -e | grep -i "LLDP" >/dev/null 2>&1; then
|
||||||
echo "-> LLDP frames observed. This indicates a neighbor device is advertising (switch/bridge)."
|
echo "-> LLDP frames observed."
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo "No control frames captured in 6s capture. That does NOT prove absence of a switch (some devices do not emit LLDP/STP)."
|
echo "No control frames captured in 6s capture."
|
||||||
fi
|
fi
|
||||||
rm -f "$TMPPCAP"
|
rm -f "$TMPPCAP" || true
|
||||||
|
trap - EXIT
|
||||||
else
|
else
|
||||||
run_header "Passive capture: tcpdump not available"
|
run_header "Passive capture: tcpdump not available"
|
||||||
echo "tcpdump missing. Install tcpdump and re-run to capture control frames (STP/LLDP)."
|
echo "tcpdump missing. Install tcpdump and re-run to capture control frames (STP/LLDP)."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 5) ethtool PHY/link partner info
|
# 5) ethtool
|
||||||
if $HAS_ETHTOOL; then
|
if $HAS_ETHTOOL; then
|
||||||
run_header "ethtool: link/partner info"
|
run_header "ethtool: link/partner info"
|
||||||
echo "(showing ethtool output for $IFACE)"
|
|
||||||
sudo ethtool "$IFACE" || true
|
sudo ethtool "$IFACE" || true
|
||||||
echo
|
|
||||||
echo "ethtool -a (autoneg/advertised/partner info) if supported:"
|
|
||||||
sudo ethtool -a "$IFACE" 2>/dev/null || true
|
sudo ethtool -a "$IFACE" 2>/dev/null || true
|
||||||
echo
|
|
||||||
echo "ethtool -S (driver stats) if supported:"
|
|
||||||
sudo ethtool -S "$IFACE" 2>/dev/null || true
|
sudo ethtool -S "$IFACE" 2>/dev/null || true
|
||||||
echo
|
|
||||||
echo "Notes: Some drivers expose PHY partner info; presence of a 'PHY' or 'link partner advertised' entry may hint at a switch PHY vs peer NIC."
|
|
||||||
else
|
else
|
||||||
run_header "ethtool not available"
|
run_header "ethtool not available"
|
||||||
echo "Install ethtool for PHY diagnostics (apt install ethtool)."
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 6) ARP / neighbor and MAC lookup
|
# 6) ARP / neighbor and MAC lookup
|
||||||
run_header "ARP table and MAC OUI"
|
run_header "ARP table and MAC OUI"
|
||||||
ip neigh show to "$TARGET_IP" | sed -n '1,50p' || true
|
ip neigh show to "$TARGET_IP" | sed -n '1,50p' || true
|
||||||
arp_mac=$(ip neigh show to "$TARGET_IP" | awk '{print $5; exit}' || true)
|
arp_mac=$(ip neigh show to "$TARGET_IP" | awk '{print $5; exit}' || true)
|
||||||
if [ -n "$arp_mac" ]; then
|
if [[ -n "$arp_mac" ]]; then
|
||||||
echo "Observed MAC for $TARGET_IP: $arp_mac"
|
echo "Observed MAC for $TARGET_IP: $arp_mac"
|
||||||
echo "OUI (first 3 octets): $(echo "$arp_mac" | awk -F: '{print toupper($1 $2 $3)}' | sed 's/\(..\)/\1:/g;s/:$//')"
|
|
||||||
else
|
else
|
||||||
echo "No ARP entry yet. Try 'arping' or ping to populate ARP."
|
echo "No ARP entry yet."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 7) arping test (if available) - see reply times, flooding behavior
|
# 7) arping
|
||||||
if $HAS_ARPING; then
|
if $HAS_ARPING; then
|
||||||
run_header "arping: 5 probes to target (shows ARP replies and timing)"
|
run_header "arping: 5 probes to target"
|
||||||
echo "(arping may require sudo)"
|
|
||||||
sudo timeout 6 arping -c 5 -I "$IFACE" "$TARGET_IP" || true
|
sudo timeout 6 arping -c 5 -I "$IFACE" "$TARGET_IP" || true
|
||||||
else
|
|
||||||
run_header "arping: not available"
|
|
||||||
echo "Install arping to run ARP-level timing tests (apt install arping)."
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 8) ping micro-latency test
|
# 8) ping micro-latency test
|
||||||
run_header "ping micro-latency test: 100 pings, 10ms interval (if allowed)"
|
run_header "ping micro-latency test: 100 pings, 10ms interval"
|
||||||
echo "(This measures latency distribution; software bridge often adds slightly higher microsecond latency.)"
|
|
||||||
ping_count=100
|
|
||||||
if ping -c 1 "$TARGET_IP" >/dev/null 2>&1; then
|
if ping -c 1 "$TARGET_IP" >/dev/null 2>&1; then
|
||||||
ping -c "$ping_count" -i 0.01 "$TARGET_IP" | tail -n 5
|
ping -c 100 -i 0.01 "$TARGET_IP" | tail -n 5 || true
|
||||||
# compute stats quickly
|
stats=$(ping -c 10 -i 0.01 -q "$TARGET_IP" 2>/dev/null | tail -n1 || true)
|
||||||
stats=$(ping -c "$ping_count" -i 0.01 -q "$TARGET_IP" 2>/dev/null | tail -n1 || true)
|
|
||||||
echo "Ping summary: $stats"
|
echo "Ping summary: $stats"
|
||||||
else
|
else
|
||||||
echo "Target is not responding to ICMP, skipping ping test."
|
echo "Target is not responding to ICMP, skipping ping test."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 9) traceroute (may help if some IP hops exist)
|
# 9) traceroute
|
||||||
if $HAS_TRACEROUTE; then
|
if $HAS_TRACEROUTE; then
|
||||||
run_header "traceroute (ICMP) to target (may not be useful for L2) -- 5 probes"
|
run_header "traceroute to target"
|
||||||
traceroute -n -w 1 -q 1 "$TARGET_IP" || true
|
traceroute -n -w 1 -q 1 "$TARGET_IP" || true
|
||||||
else
|
|
||||||
run_header "traceroute not available"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 10) passive multicast sniff for other control frames (LLDP multicast 01:80:c2:00:00:0e etc)
|
# 10) passive multicast sniff
|
||||||
if $HAS_TCPDUMP; then
|
if $HAS_TCPDUMP; then
|
||||||
run_header "Passive: sniff for LLDP multicast (01:80:c2:00:00:0e) and other bridge groups"
|
run_header "Passive: sniff for LLDP/stp multicast (4s)"
|
||||||
TMPLOG=$(mktemp /tmp/detect_bridge_log.XXXX.txt)
|
TMPLOG=$(mktemp /tmp/detect_bridge_log.XXXX.txt)
|
||||||
sudo timeout 4 tcpdump -i "$IFACE" -s 160 -l -n 'ether multicast' 2>/dev/null | sed -n '1,200p' >"$TMPLOG" || true
|
sudo timeout 4 tcpdump -i "$IFACE" -s 160 -l -n 'ether multicast' 2>/dev/null | sed -n '1,200p' >"$TMPLOG" || true
|
||||||
if [ -s "$TMPLOG" ]; then
|
if [[ -s "$TMPLOG" ]]; then
|
||||||
echo "Multicast control frames captured (sample):"
|
|
||||||
sed -n '1,50p' "$TMPLOG"
|
sed -n '1,50p' "$TMPLOG"
|
||||||
if grep -i "LLDP" "$TMPLOG" >/dev/null 2>&1; then
|
|
||||||
echo "-> LLDP present."
|
|
||||||
fi
|
|
||||||
if grep -i "STP\|BPDU" "$TMPLOG" >/dev/null 2>&1; then
|
|
||||||
echo "-> STP/BPDU present."
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
echo "No multicast control frames in short 4s sniff."
|
echo "No multicast control frames in short 4s sniff."
|
||||||
fi
|
fi
|
||||||
rm -f "$TMPLOG"
|
rm -f "$TMPLOG" || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 11) Attempt to infer switch by MAC learning / flood test (non-destructive)
|
# 11) gentle ARP burst
|
||||||
# Method: send broadcast pings (ARP floods) and observe if any change in behavior
|
run_header "Broadcast/ARP test (gentle)"
|
||||||
run_header "Broadcast/ARP flood test (gentle): send 3 ARP probes quickly and observe any flooding/delays"
|
|
||||||
if $HAS_ARPING; then
|
if $HAS_ARPING; then
|
||||||
echo "Sending 3 arping requests spaced tightly to observe behavior..."
|
|
||||||
sudo timeout 4 arping -c 3 -I "$IFACE" "$TARGET_IP" >/dev/null 2>&1 || true
|
sudo timeout 4 arping -c 3 -I "$IFACE" "$TARGET_IP" >/dev/null 2>&1 || true
|
||||||
echo "Check dmesg or bridge forwarding table on the local machine (if you manage it) for MAC learning events."
|
|
||||||
else
|
else
|
||||||
echo "arping not available -> skipping."
|
echo "arping not available -> skipping."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 12) Optional: MAC-spoof learning probe (disabled by default)
|
# 12) Optional MAC-spoof test (prompt/confirm)
|
||||||
if $MAC_SPOOF = true; then
|
if [ "$MAC_SPOOF" = true ]; then
|
||||||
echo
|
echo
|
||||||
echo "***** MAC SPOOF TEST ENABLED *****"
|
echo "***** MAC SPOOF TEST ENABLED *****"
|
||||||
echo "This will temporarily change the MAC of $IFACE to a random value and send pings to see whether traffic is forwarded/learned by a bridge."
|
read -r -p "This may disrupt link. Continue? (y/N) " yn
|
||||||
echo "If you run this, you MUST ensure you can restore connectivity (script attempts to restore original MAC)."
|
|
||||||
read -p "Continue with MAC spoof test? (y/N) " yn
|
|
||||||
if [[ "$yn" =~ ^[Yy]$ ]]; then
|
if [[ "$yn" =~ ^[Yy]$ ]]; then
|
||||||
orig_mac=$(cat /sys/class/net/"$IFACE"/address)
|
orig_mac=$(cat "/sys/class/net/$IFACE/address")
|
||||||
rand_mac=$(printf '02:%02x:%02x:%02x:%02x:%02x\n' $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256)))
|
rand_mac=$(printf '02:%02x:%02x:%02x:%02x:%02x\n' $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256)))
|
||||||
echo "Original MAC: $orig_mac"
|
echo "Original MAC: $orig_mac"
|
||||||
echo "Changing $IFACE to $rand_mac"
|
echo "Changing $IFACE to $rand_mac"
|
||||||
sudo ip link set dev "$IFACE" down
|
sudo ip link set dev "$IFACE" down
|
||||||
sudo ip link set dev "$IFACE" address "$rand_mac"
|
sudo ip link set dev "$IFACE" address "$rand_mac"
|
||||||
sudo ip link set dev "$IFACE" up
|
sudo ip link set dev "$IFACE" up
|
||||||
echo "Sending 5 pings to $TARGET_IP..."
|
|
||||||
ping -c 5 "$TARGET_IP" || true
|
ping -c 5 "$TARGET_IP" || true
|
||||||
echo "Restoring original MAC"
|
echo "Restoring original MAC"
|
||||||
sudo ip link set dev "$IFACE" down
|
sudo ip link set dev "$IFACE" down
|
||||||
@@ -285,102 +285,49 @@ if $MAC_SPOOF = true; then
|
|||||||
echo "Skipping MAC spoof test."
|
echo "Skipping MAC spoof test."
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo
|
echo "(MAC spoof test disabled. To enable, re-run with --mac-spoof.)"
|
||||||
echo "(MAC spoof test disabled. To enable, re-run with --mac-spoof and be prepared for temporary link disruption.)"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 13) Synthesize findings and give a confidence score
|
# 13) Synthesis (simple)
|
||||||
run_header "Synthesis of observations and confidence"
|
run_header "Synthesis (brief)"
|
||||||
|
|
||||||
score=0
|
score=0
|
||||||
notes=()
|
notes=()
|
||||||
|
|
||||||
# If we saw STP/BPDU -> strong evidence
|
if $HAS_TCPDUMP && sudo timeout 1 tcpdump -n -c 1 -i "$IFACE" 'ether dst 01:80:c2:00:00:00' >/dev/null 2>&1; then
|
||||||
if $HAS_TCPDUMP && sudo tcpdump -n -c 1 -i "$IFACE" 'ether dst 01:80:c2:00:00:00' >/dev/null 2>&1; then
|
|
||||||
score=$((score+40))
|
score=$((score+40))
|
||||||
notes+=("STP/BPDU frames were observed -> very likely a bridge/switch (possibly Linux bridge running STP).")
|
notes+=("STP/BPDU observed -> likely bridge/switch.")
|
||||||
fi
|
fi
|
||||||
|
if $HAS_TCPDUMP && sudo timeout 1 tcpdump -n -c 1 -i "$IFACE" 'ether proto 0x88cc' >/dev/null 2>&1; then
|
||||||
# LLDP observed
|
|
||||||
if $HAS_TCPDUMP && sudo tcpdump -n -c 1 -i "$IFACE" 'ether proto 0x88cc' >/dev/null 2>&1; then
|
|
||||||
score=$((score+40))
|
score=$((score+40))
|
||||||
notes+=("LLDP frames observed -> neighbor device is advertising (switch/bridge).")
|
notes+=("LLDP observed -> neighbor advertising.")
|
||||||
elif $HAS_LLDPC; then
|
elif $HAS_LLDPC; then
|
||||||
# try lldpctl quick check
|
|
||||||
if sudo lldpctl "$IFACE" 2>/dev/null | grep -q .; then
|
if sudo lldpctl "$IFACE" 2>/dev/null | grep -q .; then
|
||||||
score=$((score+40))
|
score=$((score+40))
|
||||||
notes+=("lldpctl shows LLDP neighbor on $IFACE -> neighbor device found (likely bridge/switch).")
|
notes+=("lldpctl shows LLDP neighbor on $IFACE.")
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
if [[ -n "$master" ]]; then
|
||||||
# ethtool hints: if ethtool prints "Link detected: yes" but partner info ambiguous; check for PHY entries
|
|
||||||
if $HAS_ETHTOOL; then
|
|
||||||
if sudo ethtool "$IFACE" 2>/dev/null | grep -qi "Link detected: yes"; then
|
|
||||||
# if driver exposes "Link partner" lines, count it as small hint
|
|
||||||
if sudo ethtool "$IFACE" 2>/dev/null | grep -i "Link partner\|PHY" >/dev/null 2>&1; then
|
|
||||||
score=$((score+5))
|
|
||||||
notes+=("ethtool shows PHY/Link-partner info -> small hint the partner may be a switch PHY.")
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ping/arp timings: if average ping latency > 0.2ms add small weight (depends on environment)
|
|
||||||
if ping -c 10 -i 0.01 -q "$TARGET_IP" >/dev/null 2>&1; then
|
|
||||||
avg_ms=$(ping -c 10 -i 0.01 -q "$TARGET_IP" 2>/dev/null | tail -n1 | awk -F'/' '{print $5}')
|
|
||||||
# if avg above threshold (0.2ms)
|
|
||||||
avg_ms_f=$(printf "%.3f" "$avg_ms")
|
|
||||||
avg_us=$(awk "BEGIN {print $avg_ms_f*1000}")
|
|
||||||
if (( $(echo "$avg_us > 200" | bc -l) )); then
|
|
||||||
score=$((score+5))
|
|
||||||
notes+=("Ping avg ${avg_ms_f} ms (≈ ${avg_us%.*} µs) — slightly higher than direct NIC-NIC; could indicate software bridging, but not conclusive.")
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ARP behavior: if ARP shows an intermediate device (rare), small weight
|
|
||||||
if [ -n "$arp_mac" ]; then
|
|
||||||
# if arp_mac OUI seems vendor-like? we can't lookup vendor offline, but if OUI ends with 00:00:00 unlikely
|
|
||||||
score=$((score+2))
|
|
||||||
notes+=("ARP resolved target's MAC (${arp_mac}). (By itself this is expected and not a proof for/against a bridge.)")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ip link master: if interface is enslaved locally -> local bridge
|
|
||||||
if [ -n "$master" ]; then
|
|
||||||
score=$((score+30))
|
score=$((score+30))
|
||||||
notes+=("Interface master indicates this host is attached to a local bridge ($master) — local bridge present.")
|
notes+=("Interface master indicates local bridge ($master).")
|
||||||
|
fi
|
||||||
|
if [[ -n "$arp_mac" ]]; then
|
||||||
|
score=$((score+2))
|
||||||
|
notes+=("ARP resolved target MAC ($arp_mac).")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# TTL/hops/traceroute: generally not useful, but check if traceroute shows multiple hops
|
if [[ "$score" -gt 100 ]]; then score=100; fi
|
||||||
if $HAS_TRACEROUTE; then
|
|
||||||
hops=$(traceroute -n -q 1 -w 1 -m 3 "$TARGET_IP" 2>/dev/null | awk 'NR>1 {print $2}' | wc -l)
|
|
||||||
if [ "$hops" -gt 1 ]; then
|
|
||||||
score=$((score+2))
|
|
||||||
notes+=("traceroute showed more than 1 hop (rare for pure L2) — inspect carefully.")
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Cap score 0-100
|
|
||||||
if [ "$score" -gt 100 ]; then score=100; fi
|
|
||||||
|
|
||||||
echo "Score: $score / 100"
|
echo "Score: $score / 100"
|
||||||
echo
|
|
||||||
echo "Notes:"
|
|
||||||
for n in "${notes[@]}"; do
|
for n in "${notes[@]}"; do
|
||||||
echo " - $n"
|
echo " - $n"
|
||||||
done
|
done
|
||||||
|
|
||||||
echo
|
if [[ "$score" -ge 70 ]]; then
|
||||||
if [ "$score" -ge 70 ]; then
|
echo "Conclusion: HIGH confidence of a bridge/switch between hosts."
|
||||||
echo "Conclusion: HIGH confidence there is a switch/bridge between the hosts."
|
elif [[ "$score" -ge 35 ]]; then
|
||||||
elif [ "$score" -ge 35 ]; then
|
echo "Conclusion: MEDIUM confidence."
|
||||||
echo "Conclusion: MEDIUM confidence of a switch/bridge between hosts. Some signals found but not definitive."
|
|
||||||
else
|
else
|
||||||
echo "Conclusion: LOW confidence. No strong evidence found; switch could still be present but silent (no LLDP/STP), or path may be direct."
|
echo "Conclusion: LOW confidence."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo
|
|
||||||
echo "Recommendations:"
|
|
||||||
echo " - If you control the intermediate device, check 'brctl show' or 'bridge link' on that host."
|
|
||||||
echo " - Enable lldpd on the bridge (sudo apt install lldpd) and use lldpctl on both ends."
|
|
||||||
echo " - If tcpdump showed STP/BPDU or LLDP, that's the most direct evidence."
|
|
||||||
echo
|
echo
|
||||||
echo "Done."
|
echo "Done."
|
||||||
Reference in New Issue
Block a user