Skip to content

Instantly share code, notes, and snippets.

@idwpan
Created November 30, 2025 04:27
Show Gist options
  • Select an option

  • Save idwpan/4b096c801c6f231e2572ca4739c552cb to your computer and use it in GitHub Desktop.

Select an option

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
#!/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