diff --git a/tools/test_bridge.sh b/tools/test_bridge.sh index 337b6dd..933fb20 100644 --- a/tools/test_bridge.sh +++ b/tools/test_bridge.sh @@ -1,84 +1,118 @@ #!/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 [interface] -# - Optional flags: -# --mac-spoof (runs an optional MAC-change test; disabled by default) +# Usage: sudo ./detect_bridge.sh [interface] [--mac-spoof] # -# 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 IFS=$'\n\t' -TARGET="$1" -IFACE="${2:-}" +# defaults +TARGET="" +IFACE="" MAC_SPOOF=false -# parse optional flags -for arg in "$@"; do - if [ "$arg" = "--mac-spoof" ]; then - MAC_SPOOF=true - fi +# --- 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 - echo "$IFACE" + if [[ -n "$IFACE" ]]; then + printf '%s\n' "$IFACE" return 0 fi - # Use 'ip route get' to find outgoing interface if route_info=$(ip route get "$TARGET" 2>/dev/null); then - # route_info often contains "dev " dev=$(echo "$route_info" | awk '{for(i=1;i<=NF;i++){if($i=="dev"){print $(i+1);exit}}}') - if [ -n "$dev" ]; then - echo "$dev" + if [[ -n "$dev" ]]; then + printf '%s\n' "$dev" return 0 fi fi - # fallback to first non-loopback up interface + # 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 - echo "$dev" + 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 - echo "eth0" + 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 -# ensure we can resolve target ip -TARGET_IP="" +# resolve target IP if [[ "$TARGET" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then TARGET_IP="$TARGET" 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 exit 1 fi fi + echo "Resolved target IP: $TARGET_IP" echo -# check for required commands and print warnings for optional commands +# 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 @@ -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 echo "Tool availability:" -echo " tcpdump: $HAS_TCPDUMP" -echo " ethtool: $HAS_ETHTOOL" -echo " arping: $HAS_ARPING" -echo " lldpctl: $HAS_LLDPC" -echo " traceroute: $HAS_TRACEROUTE" +echo " tcpdump: $HAS_TCPDUMP" +echo " ethtool: $HAS_ETHTOOL" +echo " arping: $HAS_ARPING" +echo " lldpctl: $HAS_LLDPC" +echo " traceroute: $HAS_TRACEROUTE" echo -# helper to run a command with a nice header run_header() { echo echo "===== $1 =====" } -# 1) interface->master check (fast & safe) +# 1) interface->master check run_header "Interface master / bridge hint (ip link)" -ip link show dev "$IFACE" | sed -n '1,4p' -# Look for "master " or "brd" -master=$(ip link show dev "$IFACE" 2>/dev/null | tr '\n' ' ' | sed 's/.*master \([^ ]*\).*/\1/;t;d') -if [ -n "$master" ]; then +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 (exists only when this host has a bridge device) +# 2) check /sys/class/net//bridge 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)." else echo "No /sys/class/net/$IFACE/bridge -> local host is not the bridge owner for this interface." fi -# 3) LLDP via lldpctl (if installed) +# 3) LLDP via lldpctl if $HAS_LLDPC; then - run_header "LLDP via lldpctl (if lldpd installed) -- shows neighbor(s) when available" - echo "(running: lldpctl -f keyvalue on $IFACE)" + 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. 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 -# 4) passive capture for STP/LLDP (tcpdump) - captures broadcast control frames +# 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)" - 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) + 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 into $TMPPCAP. Decoding summary (tcpdump -r):" + if [[ -s "$TMPPCAP" ]]; then + echo "Captured packets; summary:" 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 - echo "-> STP/BPDU frames observed. Very likely a bridge/switch present (could be Linux bridge running STP)." + 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. This indicates a neighbor device is advertising (switch/bridge)." + echo "-> LLDP frames observed." fi 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 - rm -f "$TMPPCAP" + 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 PHY/link partner info +# 5) ethtool if $HAS_ETHTOOL; then run_header "ethtool: link/partner info" - echo "(showing ethtool output for $IFACE)" sudo ethtool "$IFACE" || true - echo - echo "ethtool -a (autoneg/advertised/partner info) if supported:" sudo ethtool -a "$IFACE" 2>/dev/null || true - echo - echo "ethtool -S (driver stats) if supported:" 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 run_header "ethtool not available" - echo "Install ethtool for PHY diagnostics (apt install ethtool)." 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 +if [[ -n "$arp_mac" ]]; then 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 - echo "No ARP entry yet. Try 'arping' or ping to populate ARP." + echo "No ARP entry yet." fi -# 7) arping test (if available) - see reply times, flooding behavior +# 7) arping if $HAS_ARPING; then - run_header "arping: 5 probes to target (shows ARP replies and timing)" - echo "(arping may require sudo)" + run_header "arping: 5 probes to target" 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 # 8) ping micro-latency test -run_header "ping micro-latency test: 100 pings, 10ms interval (if allowed)" -echo "(This measures latency distribution; software bridge often adds slightly higher microsecond latency.)" -ping_count=100 +run_header "ping micro-latency test: 100 pings, 10ms interval" if ping -c 1 "$TARGET_IP" >/dev/null 2>&1; then - ping -c "$ping_count" -i 0.01 "$TARGET_IP" | tail -n 5 - # compute stats quickly - stats=$(ping -c "$ping_count" -i 0.01 -q "$TARGET_IP" 2>/dev/null | tail -n1 || true) + 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 (may help if some IP hops exist) +# 9) traceroute 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 -else - run_header "traceroute not available" 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 - 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) 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 - echo "Multicast control frames captured (sample):" + if [[ -s "$TMPLOG" ]]; then 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 echo "No multicast control frames in short 4s sniff." fi - rm -f "$TMPLOG" + rm -f "$TMPLOG" || true fi -# 11) Attempt to infer switch by MAC learning / flood test (non-destructive) -# Method: send broadcast pings (ARP floods) and observe if any change in behavior -run_header "Broadcast/ARP flood test (gentle): send 3 ARP probes quickly and observe any flooding/delays" +# 11) gentle ARP burst +run_header "Broadcast/ARP test (gentle)" 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 - echo "Check dmesg or bridge forwarding table on the local machine (if you manage it) for MAC learning events." else echo "arping not available -> skipping." fi -# 12) Optional: MAC-spoof learning probe (disabled by default) -if $MAC_SPOOF = true; then +# 12) Optional MAC-spoof test (prompt/confirm) +if [ "$MAC_SPOOF" = true ]; then echo 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." - 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 + read -r -p "This may disrupt link. Continue? (y/N) " yn 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))) 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 - echo "Sending 5 pings to $TARGET_IP..." ping -c 5 "$TARGET_IP" || true echo "Restoring original MAC" sudo ip link set dev "$IFACE" down @@ -285,102 +285,49 @@ if $MAC_SPOOF = true; then echo "Skipping MAC spoof test." fi else - echo - echo "(MAC spoof test disabled. To enable, re-run with --mac-spoof and be prepared for temporary link disruption.)" + echo "(MAC spoof test disabled. To enable, re-run with --mac-spoof.)" fi -# 13) Synthesize findings and give a confidence score -run_header "Synthesis of observations and confidence" - +# 13) Synthesis (simple) +run_header "Synthesis (brief)" score=0 notes=() -# If we saw STP/BPDU -> strong evidence -if $HAS_TCPDUMP && sudo tcpdump -n -c 1 -i "$IFACE" 'ether dst 01:80:c2:00:00:00' >/dev/null 2>&1; then +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 frames were observed -> very likely a bridge/switch (possibly Linux bridge running STP).") + notes+=("STP/BPDU observed -> likely bridge/switch.") fi - -# LLDP observed -if $HAS_TCPDUMP && sudo tcpdump -n -c 1 -i "$IFACE" 'ether proto 0x88cc' >/dev/null 2>&1; then +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 frames observed -> neighbor device is advertising (switch/bridge).") + notes+=("LLDP observed -> neighbor advertising.") elif $HAS_LLDPC; then - # try lldpctl quick check if sudo lldpctl "$IFACE" 2>/dev/null | grep -q .; then 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 - -# 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 +if [[ -n "$master" ]]; then 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 -# TTL/hops/traceroute: generally not useful, but check if traceroute shows multiple hops -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 - +if [[ "$score" -gt 100 ]]; then score=100; fi echo "Score: $score / 100" -echo -echo "Notes:" for n in "${notes[@]}"; do echo " - $n" done -echo -if [ "$score" -ge 70 ]; then - echo "Conclusion: HIGH confidence there is a switch/bridge between the hosts." -elif [ "$score" -ge 35 ]; then - echo "Conclusion: MEDIUM confidence of a switch/bridge between hosts. Some signals found but not definitive." +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. No strong evidence found; switch could still be present but silent (no LLDP/STP), or path may be direct." + echo "Conclusion: LOW confidence." 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 "Done." \ No newline at end of file