From 0e4dfd685988b80251617052209ce9b41aee7693 Mon Sep 17 00:00:00 2001 From: malmert Date: Mon, 9 Mar 2026 21:49:05 +0100 Subject: [PATCH] test --- tools/test_bridge.sh | 386 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 386 insertions(+) create mode 100644 tools/test_bridge.sh diff --git a/tools/test_bridge.sh b/tools/test_bridge.sh new file mode 100644 index 0000000..337b6dd --- /dev/null +++ b/tools/test_bridge.sh @@ -0,0 +1,386 @@ +#!/usr/bin/env bash +# +# detect_bridge.sh +# +# 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) +# +# 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:-}" +MAC_SPOOF=false + +# parse optional flags +for arg in "$@"; do + if [ "$arg" = "--mac-spoof" ]; then + MAC_SPOOF=true + fi +done + +# helper: detect default interface to reach target +detect_iface() { + if [ -n "$IFACE" ]; then + echo "$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" + return 0 + fi + fi + # fallback to 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" + return 0 + fi + done + echo "eth0" +} + +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="" +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 + 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 +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 + +# helper to run a command with a nice header +run_header() { + echo + echo "===== $1 =====" +} + +# 1) interface->master check (fast & safe) +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 + 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) +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 installed) +if $HAS_LLDPC; then + run_header "LLDP via lldpctl (if lldpd installed) -- shows neighbor(s) when available" + 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." +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." +fi + +# 4) passive capture for STP/LLDP (tcpdump) - captures broadcast control frames +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) + 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):" + 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)." + 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)." + 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)." + fi + rm -f "$TMPPCAP" +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 +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 + 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." +fi + +# 7) arping test (if available) - see reply times, flooding behavior +if $HAS_ARPING; then + run_header "arping: 5 probes to target (shows ARP replies and timing)" + echo "(arping may require sudo)" + 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 +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) + 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) +if $HAS_TRACEROUTE; then + run_header "traceroute (ICMP) to target (may not be useful for L2) -- 5 probes" + 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) +if $HAS_TCPDUMP; then + run_header "Passive: sniff for LLDP multicast (01:80:c2:00:00:0e) and other bridge groups" + 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):" + 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" +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" +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 + 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 + 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 + echo "Sending 5 pings to $TARGET_IP..." + 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 + echo "(MAC spoof test disabled. To enable, re-run with --mac-spoof and be prepared for temporary link disruption.)" +fi + +# 13) Synthesize findings and give a confidence score +run_header "Synthesis of observations and confidence" + +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 + score=$((score+40)) + notes+=("STP/BPDU frames were observed -> very likely a bridge/switch (possibly Linux bridge running STP).") +fi + +# LLDP observed +if $HAS_TCPDUMP && sudo 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).") +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).") + 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 + score=$((score+30)) + notes+=("Interface master indicates this host is attached to a local bridge ($master) — local bridge present.") +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 + +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." +else + echo "Conclusion: LOW confidence. No strong evidence found; switch could still be present but silent (no LLDP/STP), or path may be direct." +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