#!/usr/bin/env bash # # detect_bridge.sh -- improved / robust argument handling # # Usage: sudo ./detect_bridge.sh [interface] [--mac-spoof] # set -euo pipefail IFS=$'\n\t' # defaults TARGET="" IFACE="" MAC_SPOOF=false # --- parse args robustly --- while [[ $# -gt 0 ]]; do case "$1" in --mac-spoof) 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 < [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 if [[ -z "$TARGET" ]]; then echo "Error: target IP or hostname required." >&2 echo "Usage: sudo $0 [interface] [--mac-spoof]" >&2 exit 2 fi # helper: detect default interface to reach target detect_iface() { if [[ -n "$IFACE" ]]; then printf '%s\n' "$IFACE" return 0 fi if route_info=$(ip route get "$TARGET" 2>/dev/null); then dev=$(echo "$route_info" | awk '{for(i=1;i<=NF;i++){if($i=="dev"){print $(i+1);exit}}}') if [[ -n "$dev" ]]; then printf '%s\n' "$dev" return 0 fi fi # fallback: first non-loopback up interface 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 printf '%s\n' "$dev" return 0 fi done printf 'eth0\n' } # ensure ip exists if ! command -v ip >/dev/null 2>&1; then echo "This script requires 'ip' (iproute2). Aborting." >&2 exit 1 fi IFACE=$(detect_iface) echo "Target: $TARGET" echo "Interface: $IFACE" echo # resolve target IP if [[ "$TARGET" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then TARGET_IP="$TARGET" else TARGET_IP=$(getent hosts "$TARGET" | awk '{print $1}' | head -n1 || true) if [[ -z "$TARGET_IP" ]]; then echo "Failed to resolve target '$TARGET'." >&2 exit 1 fi fi echo "Resolved target IP: $TARGET_IP" echo # check required commands REQ_CMDS=(ip ping awk grep sed date) for c in "${REQ_CMDS[@]}"; do if ! command -v "$c" >/dev/null 2>&1; then echo "Missing required command: $c" >&2 exit 1 fi done # optional commands HAS_TCPDUMP=false HAS_ETHTOOL=false HAS_ARPING=false HAS_LLDPC=false HAS_TRACEROUTE=false if command -v tcpdump >/dev/null 2>&1; then HAS_TCPDUMP=true; fi if command -v ethtool >/dev/null 2>&1; then HAS_ETHTOOL=true; fi if command -v arping >/dev/null 2>&1; then HAS_ARPING=true; fi 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 echo "Tool availability:" echo " tcpdump: $HAS_TCPDUMP" echo " ethtool: $HAS_ETHTOOL" echo " arping: $HAS_ARPING" echo " lldpctl: $HAS_LLDPC" echo " traceroute: $HAS_TRACEROUTE" echo run_header() { echo echo "===== $1 =====" } # 1) interface->master check run_header "Interface master / bridge hint (ip link)" ip link show dev "$IFACE" | sed -n '1,4p' || true master=$(ip link show dev "$IFACE" 2>/dev/null | tr '\n' ' ' | sed 's/.*master \([^ ]*\).*/\1/;t;d' || true) if [[ -n "$master" ]]; then echo "Interface reports master: $master -> This interface is enslaved to a bridge on this host (local bridge)." else echo "No 'master' shown; interface is not a local bridge slave (or not reported)." fi # 2) check /sys/class/net//bridge run_header "Check local bridge sysfs" 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)." else echo "No /sys/class/net/$IFACE/bridge -> local host is not the bridge owner for this interface." fi # 3) LLDP via lldpctl if $HAS_LLDPC; then run_header "LLDP via lldpctl" sudo lldpctl -f keyvalue "$IFACE" 2>/dev/null || echo "lldpctl produced no output or isn't running for $IFACE." else run_header "LLDP: lldpctl not installed" echo "lldpctl not installed. Install lldpd and run lldpctl to try LLDP discovery." fi # 4) passive capture for STP/LLDP (tcpdump) if $HAS_TCPDUMP; then run_header "Passive capture: look for STP BPDUs and LLDP frames (tcpdump, 6s capture)" 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 if [[ -s "$TMPPCAP" ]]; then echo "Captured packets; summary:" sudo tcpdump -n -r "$TMPPCAP" -e | sed -n '1,50p' || true if sudo tcpdump -n -r "$TMPPCAP" -e | grep -i "stp\|bpdu" >/dev/null 2>&1; then echo "-> STP/BPDU frames observed." fi if sudo tcpdump -n -r "$TMPPCAP" -e | grep -i "LLDP" >/dev/null 2>&1; then echo "-> LLDP frames observed." fi else echo "No control frames captured in 6s capture." fi rm -f "$TMPPCAP" || true trap - EXIT else run_header "Passive capture: tcpdump not available" echo "tcpdump missing. Install tcpdump and re-run to capture control frames (STP/LLDP)." fi # 5) ethtool if $HAS_ETHTOOL; then run_header "ethtool: link/partner info" sudo ethtool "$IFACE" || true sudo ethtool -a "$IFACE" 2>/dev/null || true sudo ethtool -S "$IFACE" 2>/dev/null || true else run_header "ethtool not available" fi # 6) ARP / neighbor and MAC lookup run_header "ARP table and MAC OUI" ip neigh show to "$TARGET_IP" | sed -n '1,50p' || true arp_mac=$(ip neigh show to "$TARGET_IP" | awk '{print $5; exit}' || true) if [[ -n "$arp_mac" ]]; then echo "Observed MAC for $TARGET_IP: $arp_mac" else echo "No ARP entry yet." fi # 7) arping if $HAS_ARPING; then run_header "arping: 5 probes to target" sudo timeout 6 arping -c 5 -I "$IFACE" "$TARGET_IP" || true fi # 8) ping micro-latency test run_header "ping micro-latency test: 100 pings, 10ms interval" if ping -c 1 "$TARGET_IP" >/dev/null 2>&1; then ping -c 100 -i 0.01 "$TARGET_IP" | tail -n 5 || true stats=$(ping -c 10 -i 0.01 -q "$TARGET_IP" 2>/dev/null | tail -n1 || true) echo "Ping summary: $stats" else echo "Target is not responding to ICMP, skipping ping test." fi # 9) traceroute if $HAS_TRACEROUTE; then run_header "traceroute to target" traceroute -n -w 1 -q 1 "$TARGET_IP" || true fi # 10) passive multicast sniff if $HAS_TCPDUMP; then run_header "Passive: sniff for LLDP/stp multicast (4s)" 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 if [[ -s "$TMPLOG" ]]; then sed -n '1,50p' "$TMPLOG" else echo "No multicast control frames in short 4s sniff." fi rm -f "$TMPLOG" || true fi # 11) gentle ARP burst run_header "Broadcast/ARP test (gentle)" if $HAS_ARPING; then sudo timeout 4 arping -c 3 -I "$IFACE" "$TARGET_IP" >/dev/null 2>&1 || true else echo "arping not available -> skipping." fi # 12) Optional MAC-spoof test (prompt/confirm) if [ "$MAC_SPOOF" = true ]; then echo echo "***** MAC SPOOF TEST ENABLED *****" read -r -p "This may disrupt link. Continue? (y/N) " yn if [[ "$yn" =~ ^[Yy]$ ]]; then 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))) echo "Original MAC: $orig_mac" echo "Changing $IFACE to $rand_mac" sudo ip link set dev "$IFACE" down sudo ip link set dev "$IFACE" address "$rand_mac" sudo ip link set dev "$IFACE" up ping -c 5 "$TARGET_IP" || true echo "Restoring original MAC" sudo ip link set dev "$IFACE" down sudo ip link set dev "$IFACE" address "$orig_mac" sudo ip link set dev "$IFACE" up echo "MAC restored to $orig_mac" else echo "Skipping MAC spoof test." fi else echo "(MAC spoof test disabled. To enable, re-run with --mac-spoof.)" fi # 13) Synthesis (simple) run_header "Synthesis (brief)" score=0 notes=() 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 score=$((score+40)) notes+=("STP/BPDU observed -> likely bridge/switch.") fi if $HAS_TCPDUMP && sudo timeout 1 tcpdump -n -c 1 -i "$IFACE" 'ether proto 0x88cc' >/dev/null 2>&1; then score=$((score+40)) notes+=("LLDP observed -> neighbor advertising.") elif $HAS_LLDPC; then if sudo lldpctl "$IFACE" 2>/dev/null | grep -q .; then score=$((score+40)) notes+=("lldpctl shows LLDP neighbor on $IFACE.") fi fi if [[ -n "$master" ]]; then score=$((score+30)) notes+=("Interface master indicates local bridge ($master).") fi if [[ -n "$arp_mac" ]]; then score=$((score+2)) notes+=("ARP resolved target MAC ($arp_mac).") fi if [[ "$score" -gt 100 ]]; then score=100; fi echo "Score: $score / 100" for n in "${notes[@]}"; do echo " - $n" done if [[ "$score" -ge 70 ]]; then echo "Conclusion: HIGH confidence of a bridge/switch between hosts." elif [[ "$score" -ge 35 ]]; then echo "Conclusion: MEDIUM confidence." else echo "Conclusion: LOW confidence." fi echo echo "Done."