Created
November 30, 2025 04:27
-
-
Save idwpan/4b096c801c6f231e2572ca4739c552cb to your computer and use it in GitHub Desktop.
Start a remote packet capture on Linux router by piping tcpdump to wireshark over SSH
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| ROUTER_HOST="10.0.0.1" | |
| ROUTER_USER="ubnt" | |
| LISTEN_INTERFACE="switch0" | |
| INCLUDE_HOSTS=() | |
| EXCLUDE_HOSTS=() | |
| INCLUDE_PORTS=() | |
| EXCLUDE_PORTS=() | |
| EXTRA_FILTER="" | |
| TCPDUMP_ARGS=( | |
| "--packet-buffered" # write packets immediately | |
| # "-n" # don't convert addresses to names | |
| "-w -" # write raw packets to stdout | |
| ) | |
| TCPDUMP_ARGS="${TCPDUMP_ARGS[@]}" | |
| WIRESHARK_ARGS=( | |
| "-k" # Start capture immediately | |
| "--interface -" # Read from stdin | |
| ) | |
| WIRESHARK_ARGS="${WIRESHARK_ARGS[@]}" | |
| usage() { | |
| cat <<EOF | |
| Start a remote packet capture by piping tcpdump to wireshark over SSH. | |
| It is recommended to copy an SSH key to the remote machine for automatic logins. | |
| Usage: $0 [options] | |
| Capture source: | |
| -r <ip_address> Remote host (default ${ROUTER_HOST}) | |
| -u <user> SSH username (default ${ROUTER_USER}) | |
| -i <interface> Network interface to capture (default ${LISTEN_INTERFACE}) | |
| Filters: | |
| -H <host> Include host (repeatable) | |
| -XH <host> Exclude host (repeatable) | |
| Note: -H and -XH cannot be used together. | |
| -p <port> Include port (repeatable) | |
| -Xp <port> Exclude port (repeatable) | |
| Note: -p and -Xp cannot be used together. | |
| -e <expr> Extend tcpdump filter with raw expression. | |
| Configuration: | |
| -t "<tcpdump args>" Extend tcpdump args | |
| Default: ${TCPDUMP_ARGS} | |
| -w "<wireshark args>" Extend Wireshark args | |
| Default: ${WIRESHARK_ARGS} | |
| Misc: | |
| -h Show this help | |
| Examples: | |
| Only capture these hosts: | |
| $0 -H 10.0.0.50 -H 10.0.0.60 | |
| → ( host 10.0.0.50 or host 10.0.0.60 ) | |
| These hosts on these ports: | |
| $0 -H 10.0.0.50 -H 10.0.0.60 -p 22 -p 80 | |
| → ( host 10.0.0.50 or host 10.0.0.60 ) and ( port 22 or port 80 ) | |
| This host, but exclude SSH: | |
| $0 -H 10.0.0.50 -Xp 22 | |
| → ( host 10.0.0.50 ) and not ( port 22 ) | |
| Custom tcpdump or Wireshark options: | |
| $0 -H 10.0.0.50 -t "-nn" -w "--display-filter tcp" | |
| EOF | |
| exit 1 | |
| } | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| -r ) ROUTER_HOST="$2"; shift 2 ;; | |
| -u ) ROUTER_USER="$2"; shift 2 ;; | |
| -i ) LISTEN_INTERFACE="$2"; shift 2 ;; | |
| -H ) INCLUDE_HOSTS+=("$2"); shift 2 ;; | |
| -XH) EXCLUDE_HOSTS+=("$2"); shift 2 ;; | |
| -p ) INCLUDE_PORTS+=("$2"); shift 2 ;; | |
| -Xp) EXCLUDE_PORTS+=("$2"); shift 2 ;; | |
| -e ) EXTRA_FILTER="$2"; shift 2 ;; | |
| -t ) TCPDUMP_ARGS="${TCPDUMP_ARGS} ${2}"; shift 2 ;; | |
| -w ) WIRESHARK_ARGS="${WIRESHARK_ARGS} ${2}"; shift 2 ;; | |
| -h ) usage ;; | |
| * ) usage ;; | |
| esac | |
| done | |
| # Enforce mutual exclusivity | |
| if (( ${#INCLUDE_HOSTS[@]} > 0 && ${#EXCLUDE_HOSTS[@]} > 0 )); then | |
| echo "[!] Error: -H and -XH cannot be used together in the same command." >&2 | |
| exit 1 | |
| elif (( ${#INCLUDE_PORTS[@]} > 0 && ${#EXCLUDE_PORTS[@]} > 0 )); then | |
| echo "[!] Error: -p and -Xp cannot be used together in the same command." >&2 | |
| exit 1 | |
| fi | |
| ROUTER_USERHOST="${ROUTER_USER}@${ROUTER_HOST}" | |
| SSH_CMD="ssh -q ${ROUTER_USERHOST}" | |
| TCPDUMP_BIN="tcpdump" | |
| TCPDUMP_ARGS="-i ${LISTEN_INTERFACE} ${TCPDUMP_ARGS}" | |
| TCPDUMP_CMD="sudo ${TCPDUMP_BIN} ${TCPDUMP_ARGS}" | |
| WIRESHARK_BIN="wireshark" | |
| WIRESHARK_CMD="${WIRESHARK_BIN} ${WIRESHARK_ARGS}" | |
| # verify tcpdump and wireshark are available | |
| if ! $SSH_CMD "sudo -s command -v ${TCPDUMP_BIN} &>/dev/null"; then | |
| echo "[!] Error: ${TCPDUMP_BIN} not available at ${ROUTER_USERHOST}" | |
| exit 1 | |
| fi | |
| if ! command -v "${WIRESHARK_BIN}" &> /dev/null; then | |
| echo "[!] Error: ${WIRESHARK_BIN} not available on local system" | |
| exit 1 | |
| fi | |
| build_or_group() { | |
| # $1 = prefix ("host", "port") | |
| local prefix="$1"; shift | |
| local vals=("$@") | |
| local expr="" | |
| if ((${#vals[@]} == 0)); then | |
| echo "" | |
| return | |
| fi | |
| expr="${prefix} ${vals[0]}" | |
| for ((i=1; i<${#vals[@]}; i++)); do | |
| expr="${expr} or ${prefix} ${vals[i]}" | |
| done | |
| echo "( ${expr} )" | |
| } | |
| # Include groups | |
| include_hosts_expr="" | |
| include_ports_expr="" | |
| if ((${#INCLUDE_HOSTS[@]} > 0)); then | |
| include_hosts_expr=$(build_or_group "host" "${INCLUDE_HOSTS[@]}") | |
| fi | |
| if ((${#INCLUDE_PORTS[@]} > 0)); then | |
| include_ports_expr=$(build_or_group "port" "${INCLUDE_PORTS[@]}") | |
| fi | |
| # Exclude groups | |
| exclude_hosts_expr="" | |
| exclude_ports_expr="" | |
| if ((${#EXCLUDE_HOSTS[@]} > 0)); then | |
| ex_hosts=$(build_or_group "host" "${EXCLUDE_HOSTS[@]}") | |
| exclude_hosts_expr="not ${ex_hosts}" | |
| fi | |
| if ((${#EXCLUDE_PORTS[@]} > 0)); then | |
| ex_ports=$(build_or_group "port" "${EXCLUDE_PORTS[@]}") | |
| exclude_ports_expr="not ${ex_ports}" | |
| fi | |
| # Combine filter expressions | |
| parts=() | |
| if [[ -n "${include_hosts_expr}" ]]; then | |
| parts+=( "${include_hosts_expr}" ) | |
| elif [[ -n "${exclude_hosts_expr}" ]]; then | |
| parts+=( "${exclude_hosts_expr}" ) | |
| fi | |
| if [[ -n "${include_ports_expr}" ]]; then | |
| parts+=( "${include_ports_expr}" ) | |
| elif [[ -n "${exclude_ports_expr}" ]]; then | |
| parts+=( "${exclude_ports_expr}" ) | |
| fi | |
| if [[ -n "${EXTRA_FILTER}" ]]; then | |
| parts+=( "${EXTRA_FILTER}" ) | |
| fi | |
| final_expr="" | |
| for part in "${parts[@]}"; do | |
| if [[ -z "${final_expr}" ]]; then | |
| final_expr="${part}" | |
| else | |
| final_expr="( ${final_expr} ) and ( ${part} )" | |
| fi | |
| done | |
| remote_filter="" | |
| if [[ -n "${final_expr}" ]]; then | |
| remote_filter=$(printf "%q" "${final_expr}") | |
| TCPDUMP_CMD="${TCPDUMP_CMD} ${remote_filter}" | |
| fi | |
| echo "Router: ${ROUTER_USERHOST}" | |
| echo "Interface: ${LISTEN_INTERFACE}" | |
| echo "Filter: ${final_expr:-<none>}" | |
| ${SSH_CMD} "${TCPDUMP_CMD}" | ${WIRESHARK_CMD} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment