├── screenshot.png ├── README.md └── fritzbox-capture.sh /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rob--W/fritzbox-extcap-wireshark/HEAD/screenshot.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fritzbox external capture interface for Wireshark 2 | 3 | Wireshark extcap plugin to capture network traffic from FRITZ!Box routers. 4 | 5 | The `fritzbox-capture.sh` script provided here relies on the hidden packet 6 | capturing functionality built into FRITZ!Box routers, and requires admin 7 | credentials of the Fritzbox. To protect your admin credentials, the plugin 8 | does not ask for your password, but the temporary session id from Fritzbox. 9 | 10 | 11 | ## Installation 12 | 13 | Drop a copy of `fritzbox-capture.sh` in `~/.local/lib/wireshark/extcap/`, 14 | or whatever is displayed as "Personal Extcap path:" when running `tshark -G folders`. 15 | Or clone this repository and create a symlink to the file: 16 | 17 | ```sh 18 | git clone https://github.com/Rob--W/fritzbox-extcap-wireshark 19 | cd fritzbox-extcap-wireshark 20 | ln -s "$PWD/fritzbox-capture.sh" ~/.local/lib/wireshark/extcap/fritzbox-capture.sh 21 | ``` 22 | 23 | To detect this new script, press F5. Or quit and start Wireshark. 24 | 25 | 26 | ## Usage 27 | 28 | 1. Open Wireshark, and in the Interfaces section, find the "FRITZ!Box Capture: fritzbox-capture" option. 29 | 2. Press on the Settings button before that. You need to provide the following inputs: 30 | 31 | - **FRITZ!Box IP address**: The IP address of your Fritzbox router. 32 | - **FRITZ!Box interface**: The network interface of the router to capture from. 33 | Here are some examples from FRITZ!Box 7530: 34 | 35 | | FRITZ!Box interface | description | 36 | | - | - | 37 | | 1-lan | LAN | 38 | | 1-eth0 | Physical LAN1 port | 39 | | 1-eth1 | Physical LAN2 port | 40 | | 1-eth2 | Physical LAN3 port | 41 | | 1-eth3 | Physical LAN4 port | 42 | | 2-1 | Internet connection | 43 | 44 | To find more specific interfaces, visit https://fritz.box/?lp=cap 45 | For a public list from an unrelated third party, see https://github.com/jpluimers/fritzcap/blob/master/fritzcap-interfaces-table.md 46 | 47 | - **FRITZ!Box sid or URL with sid**: Log in to your fritzbox and copy any 48 | of the links containing "sid=", for example one of the sidebar links. 49 | 50 | When the session expires, you need to log in and provide this information 51 | again. The sid is asked instead of the admin password, to prevent you from 52 | accidentally saving your router's admin password in Wireshark. 53 | 54 | - There is an optional checkbox to enable logging for debugging. 55 | 56 | 3. Optionally, specify a capture filer to only select specific packets. For 57 | example, `host 192.168.178.2` to see all traffic going to that host. Filter 58 | syntax is documented at https://www.tcpdump.org/manpages/pcap-filter.7.html 59 | 60 | 4. Press Start in the dialog, or save the changes and double-click on the 61 | "FRITZ!Box Capture: fritzbox-capture" in Wireshark's Interfaces section to 62 | start capturing. 63 | 64 | 65 | ## Screenshot 66 | ![Screenshot of the config before capture](screenshot.png) 67 | -------------------------------------------------------------------------------- /fritzbox-capture.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # (c) Rob Wu 4 | # 5 | # https://github.com/Rob--W/fritzbox-extcap-wireshark 6 | 7 | # Put/symlink this file in ~/.local/lib/wireshark/extcap/ 8 | # https://www.wireshark.org/docs/wsdg_html_chunked/ChCaptureExtcap.html 9 | 10 | set -euo pipefail 11 | 12 | # Configurable variables 13 | 14 | # fritz.box resolves here: 15 | FRITZ_BOX_IP=192.168.178.1 16 | 17 | # Potentially interesting - found by visiting https://$FRITZ_BOX_IP/?lp=cap and inspecting the buttons. 18 | # There is also a listing at https://github.com/jpluimers/fritzcap/blob/master/fritzcap-interfaces-table.md 19 | # 1-eth0= Physical LAN1 port 20 | # 1-eth1= Physical LAN2 port 21 | # 1-eth2= Physical LAN3 port 22 | # 1-eth3= Physical LAN4 port 23 | # 1-lan = lan 24 | # 2-1 = 1. Internet connection 25 | # 3-17 = Interface 0 ('internet') 26 | # 4-135 = AP2 (5 GHz, ath1) - Interface 1 27 | # 4-133 = AP (2.4 GHz, ath0) - Interface 1 28 | # 4-128 = WLAN Management Traffic - Interface 0 29 | FRITZ_BOX_IFACE="1-lan" 30 | 31 | FRITZ_BOX_SID= 32 | 33 | CAPTURE_FILTER= 34 | 35 | FIFO_FILE= 36 | FIFO_FILE_CLOSED= 37 | 38 | DEBUG= 39 | DEBUG_LOG_FILE=/tmp/fritz-extcap.log 40 | log_debug() { 41 | if [ -n "$DEBUG" ] && [ -n "$DEBUG_LOG_FILE" ] ; then 42 | printf "[$$][% 5d] %s\n" "$SECONDS" "$*" >> "$DEBUG_LOG_FILE" 43 | fi 44 | } 45 | 46 | 47 | # Constants 48 | EXTCAP_VERSION=0.2 49 | CURL_BIN=/usr/bin/curl 50 | TCPDUMP_BIN=/usr/bin/tcpdump 51 | 52 | # --help 53 | showhelp() { 54 | # Note: Unlike the others, this is not part of the extcap interface and we will be a bit more liberal, 55 | # and not use absolute paths to common commands. 56 | local PROG=$(realpath "${BASH_SOURCE[0]}") 57 | cat <: specify the extcap interface 71 | --extcap-config: list the additional configuration for an interface 72 | --capture: run the capture 73 | --extcap-capture-filter : the capture filter 74 | --fifo : dump data to file or fifo 75 | --help: print this help 76 | --version: print the version 77 | --arg-ip : IP address of FRITZ!Box router. Defaults to: $FRITZ_BOX_IP 78 | --arg-iface : Network interface. Defaults to: $FRITZ_BOX_IFACE 79 | Visit https://$FRITZ_BOX_IP/?lp=cap and inspect the value of "Start" buttons to find them. 80 | Some values are also listed at https://github.com/jpluimers/fritzcap/blob/master/fritzcap-interfaces-table.md 81 | --arg-sid : URL with SID or just SID. Log in to the FRITZ!Box and copy a link containing sid. Required. 82 | --enable-logging: Enable logging 83 | --log-file : Location of log file. Defaults to: $DEBUG_LOG_FILE 84 | 85 | For usage within Wireshark, see https://github.com/Rob--W/fritzbox-extcap-wireshark#readme 86 | HELP 87 | } 88 | 89 | # --version 90 | showversion() { 91 | echo "fritzbox-capture v$EXTCAP_VERSION" 92 | } 93 | 94 | # --extcap-interfaces 95 | extcap_interfaces() { 96 | echo "extcap {version=$EXTCAP_VERSION}" # {help=helpurl} if I have one. 97 | echo "interface {value=fritzbox-capture}{display=FRITZ!Box capture}" 98 | } 99 | 100 | # --extcap-dlts 101 | extcap_dlts() { 102 | # Have no idea what to put here, neither the documentation nor Wireshark's source code explains it well. 103 | # User-defined "DLT" need to start at 147. 104 | echo "dlt {number=147}{name=fritzbox-capture}{display=The Only FRITZ!Box capture}" 105 | } 106 | 107 | # --extcap_config 108 | extcap_config() { 109 | echo "arg {number=0}{call=--arg-ip}{display=FRITZ!Box IP address}{type=string}{tooltip=FRITZ!Box IP address}{default=$FRITZ_BOX_IP}" 110 | echo "arg {number=1}{call=--arg-iface}{display=FRITZ!Box interface}{type=string}{tooltip=FRITZ!Box capture interface, see https://$FRITZ_BOX_IP/?lp=cap for more info (and inspect the buttons to see the value) }{default=$FRITZ_BOX_IFACE}" 111 | echo "arg {number=2}{call=--arg-sid}{display=FRITZ!Box sid or URL with sid}{type=string}{tooltip=The FRITZ!Box sid - after logging, find and copy a link containing sid}" 112 | echo "arg {number=3}{call=--enable-logging}{display=Enable logging for debugging}{type=boolflag}" 113 | echo "arg {number=4}{call=--log-file}{display=Location of log file}{type=string}{default=$DEBUG_LOG_FILE}" 114 | } 115 | 116 | extcap_capture_filter_validation() { 117 | # Validation: 118 | # - non-zero exit code only = greenish = unknown 119 | # - empty stdout = green = valid 120 | # - non-empty stdout = red = invalid 121 | # 122 | # Minimal eth capture file with 0 packets 123 | local DUMMY_PCAP_DATA='4\xcd\xb2\xa1\x02\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00i\x00\x00\x00' 124 | if ! printf "$DUMMY_PCAP_DATA" | "$TCPDUMP_BIN" -r- -- "$CAPTURE_FILTER" 2>/dev/null ; then 125 | echo "Invalid capture filter" 126 | fi 127 | } 128 | 129 | # One of the above functions or "capture" or "exit". 130 | ACTION= 131 | 132 | while [[ $# -gt 0 ]] ; do 133 | case $1 in 134 | --help|-h) 135 | ACTION=showhelp 136 | break 137 | ;; 138 | --version) 139 | ACTION=showversion 140 | break 141 | ;; 142 | --extcap-interfaces) 143 | ACTION=extcap_interfaces 144 | ;; 145 | --extcap-version=*) 146 | # Ignore - we support all versions. 147 | ;; 148 | --extcap-dlts) 149 | ACTION=extcap_dlts 150 | ;; 151 | --extcap-config) 152 | ACTION=extcap_config 153 | ;; 154 | --extcap-interface) 155 | # We only support one interface (and also use that for easier arg parsing) 156 | if [[ "$2" != fritzbox-capture ]] ; then 157 | echo "Unsupported interface: $2 (expected fritzbox-capture)" 158 | exit 1 159 | fi 160 | shift 161 | ;; 162 | --capture) 163 | ACTION=capture 164 | ;; 165 | --extcap-capture-filter) 166 | CAPTURE_FILTER=$2 167 | [ -z "$ACTION" ] && ACTION=extcap_capture_filter_validation 168 | shift 169 | ;; 170 | --fifo) 171 | FIFO_FILE=$2 172 | shift 173 | ;; 174 | --arg-ip) 175 | FRITZ_BOX_IP=$2 176 | shift 177 | ;; 178 | --arg-iface) 179 | FRITZ_BOX_IFACE=$2 180 | shift 181 | ;; 182 | --arg-sid) 183 | FRITZ_BOX_SID=$2 184 | shift 185 | ;; 186 | --enable-logging) 187 | DEBUG=1 188 | ;; 189 | --log-file) 190 | DEBUG_LOG_FILE=$2 191 | shift 192 | ;; 193 | --) 194 | shift 195 | break 196 | ;; 197 | *) 198 | echo "Unknown option $1" >&2 199 | ;; 200 | esac 201 | shift 202 | done 203 | 204 | [ -z "$ACTION" ] && ACTION=showhelp 205 | if [[ $ACTION != capture ]] ; then 206 | $ACTION 207 | exit $? 208 | fi 209 | 210 | # The main meat of this program: capturing 211 | 212 | if [ ! -e "$FIFO_FILE" ] ; then 213 | echo "FIFO pipe is missing" >&2 214 | exit 1 215 | fi 216 | 217 | # wireshark/extcap.c sends SIGTERM when the capture ends, after unlinking FIFO_FILE. 218 | # This is supported since Wireshark 3.3.0. Before that, the logic works with kill, 219 | # by relying on failures from FIFO_FILE. 220 | trap 'catch_signal SIGINT' SIGINT 221 | trap 'catch_signal SIGTERM' SIGTERM 222 | exited_with_exitcode= 223 | PID_OF_CURL_CAPTURE= 224 | catch_signal() { 225 | local SIGNAL=$1 226 | trap - SIGINT SIGTERM 227 | log_debug "Received $SIGNAL, cleaning up" 228 | 229 | exec 3>&- # Close FIFO_FILE fd 230 | FIFO_FILE_CLOSED=1 231 | 232 | if [ -z "$PID_OF_CURL_CAPTURE" ] ; then 233 | log_debug "Early exit because capture has not started yet" 234 | # Haven't even started capturing yet, no need to clean up/tear down. 235 | exit 2 236 | fi 237 | 238 | if [ -n "$exited_with_exitcode" ] ; then 239 | log_debug "Cleanup already started, returning control to end" 240 | # If we are already at the final cleanup, let it finish. 241 | return 242 | fi 243 | 244 | if kill "-$SIGNAL" "$PID_OF_CURL_CAPTURE" ; then 245 | log_debug "Sent signal to kill curl" 246 | else 247 | log_debug "Failed to send signal to kill curl" 248 | fi 249 | # Transfer control back to after the wait call. 250 | } 251 | 252 | echo_error_and_exit() { 253 | local ERRMSG=$1 254 | 255 | echo "$ERRMSG" >&2 256 | log_debug "$ERRMSG" 257 | 258 | # Send EOF to FIFO pipe to get Wireshark to immediately show stderr in a dialog, 259 | # instead of requiring the user to end the capture first. 260 | echo > "$FIFO_FILE" 261 | 262 | exit 1 263 | } 264 | 265 | # Convenience: Paste URL with sid and we will extract the sid. 266 | if [[ "$FRITZ_BOX_SID" =~ sid=([a-f0-9]{16}) ]]; then 267 | FRITZ_BOX_SID=${BASH_REMATCH[1]} 268 | elif [[ "$FRITZ_BOX_SID" =~ sid=([a-z0-9]+) ]]; then 269 | # Just in case the sid becomes a bit longer or shorter. 270 | # Empirically, the sid appears to be truncated at 16 characters. 271 | FRITZ_BOX_SID=${BASH_REMATCH[1]} 272 | fi 273 | 274 | # Intentionally do not validate the SID beyond the above convenience method. 275 | # Only do the bare minimum: sid check 276 | if [ -z "$FRITZ_BOX_SID" ]; then 277 | echo_error_and_exit "sid is missing. Log in at https://${FRITZ_BOX_IP} and find a link containing '?sid=' to paste it in the capture options." 278 | fi 279 | 280 | log_debug "Verifying that sid is valid" 281 | 282 | # Verify that SID is OK 283 | # This is an intentionally lax check, to allow anything that looks potentially valid. 284 | # If the validation somehow fails, then we will fail soon enough, below. 285 | RESPONSE_TEXT=$("$CURL_BIN" --silent --insecure "https://${FRITZ_BOX_IP}/?sid=${FRITZ_BOX_SID}") || log_debug "curl failed with $?" 286 | if [[ "$RESPONSE_TEXT" != *"?sid="* && "$RESPONSE_TEXT" != *'"sid":"'"$FRITZ_BOX_SID"'"'* ]] ; then 287 | echo_error_and_exit "Invalid sid ($FRITZ_BOX_SID), please log in again at https://${FRITZ_BOX_IP}" 288 | fi 289 | 290 | log_debug "SID seems valid" 291 | 292 | # Terminating the connection does not stop the capture. 293 | stop_capture_from_fritz() { 294 | local curl_exit_code=0 295 | local RESPONSE_TEXT_ON_STOP 296 | # Lower default max time down from 60 seconds. It should complete within a second, but when the fritzbox is stuck, it won't. 297 | # I have observed the fritzbox being stuck when there was an existing curl client that was likely stuck. 298 | # Killing that curl instance resolved the issue. Restarting the fritzbox always works too. 299 | RESPONSE_TEXT_ON_STOP=$("$CURL_BIN" --insecure --max-time 5 --silent "https://${FRITZ_BOX_IP}/cgi-bin/capture_notimeout?sid=${FRITZ_BOX_SID}&capture=Stop&ifaceorminor=${FRITZ_BOX_IFACE}") || curl_exit_code=$? 300 | # TODO: Should check response text? When sid is invalid, request is 200 OK with body 301 | # "Internal communication error (login 0). Exiting." 302 | # Since the capture seems to usually end when the stream stops, let's just not validate it. 303 | if [ "$curl_exit_code" == 0 ] ; then 304 | return 305 | fi 306 | if [ "$curl_exit_code" == 56 ] ; then 307 | # Despite getting the full response from the server, curl may fail with: 308 | # curl: (56) OpenSSL SSL_read: OpenSSL/3.5.2: error:0A000126:SSL routines::unexpected eof while reading, errno 0 309 | # (observed with curl 8.15.0 and Fritz!OS 8.02.) 310 | if [ -z "$RESPONSE_TEXT_ON_STOP" ] ; then 311 | # Upon closing successfully, HTTP status code is 200 and the response body is empty. 312 | log_debug "curl returned 56. Assuming that it succeeded" 313 | return 314 | fi 315 | fi 316 | log_debug "curl request to stop capture failed with $curl_exit_code" 317 | if [ -n "$RESPONSE_TEXT_ON_STOP" ] ; then 318 | log_debug "server responded with: $RESPONSE_TEXT_ON_STOP" 319 | fi 320 | return $curl_exit_code 321 | } 322 | 323 | log_debug "Stopping existing capture, if any." 324 | 325 | # Sometimes, stopping won't work, and the fritzbox won't serve new clients (i.e. immediately end the Start request). 326 | if ! stop_capture_from_fritz ; then 327 | log_debug "Failed to stop remote capture before starting a new one. There may be another capturing client." 328 | failed_to_stop_capture_at_start=1 329 | else 330 | failed_to_stop_capture_at_start= 331 | fi 332 | 333 | CURL_CAPTURE_FROM_FRITZ=( 334 | "$CURL_BIN" 335 | --insecure 336 | --silent 337 | --no-buffer 338 | "https://${FRITZ_BOX_IP}/cgi-bin/capture_notimeout?sid=${FRITZ_BOX_SID}&capture=Start&snaplen=&ifaceorminor=${FRITZ_BOX_IFACE}" 339 | ) 340 | 341 | # Open FIFO as file descriptor (write-only) rather than passing its name to curl/tcpdump, so that we can control when to close it. 342 | exec 3>"$FIFO_FILE" 343 | 344 | TIME_STARTED=$SECONDS 345 | if [ -z "$CAPTURE_FILTER" ] ; then 346 | log_debug "Starting capture without filter" 347 | "${CURL_CAPTURE_FROM_FRITZ[@]}" >&3 & 348 | PID_OF_CURL_CAPTURE=$! 349 | else 350 | log_debug "Starting capture with filter" 351 | # With a filter, it's more involved... We want to be able to kill curl ASAP when capturing stops. 352 | # When curl is just piped to tcpdump, we would only be able to kill tcpdump. 353 | # and curl would stay alive until it unsuccessfully tries to write to the pipe. 354 | # 355 | # But if we kill curl, and Wireshark closes the FIFO pipe, then tcpdump will exit automatically too. 356 | # 357 | # Under normal runs we have two situations: 358 | # - curl output ends (e.g. capture stopped). Then tcpdump would exit normally too. 359 | # - curl output is not pcap. tcpdump will detect this and exit. curl tries to write to broken pipe and fails. 360 | "${CURL_CAPTURE_FROM_FRITZ[@]}" > >("$TCPDUMP_BIN" --immediate-mode --dont-verify-checksums -nNS -s0 -r- -w- -- "$CAPTURE_FILTER" >&3 2>/dev/null) & 361 | PID_OF_CURL_CAPTURE=$! 362 | fi 363 | 364 | # Wait for them to exit, or for catch_signal to be called. 365 | if wait $PID_OF_CURL_CAPTURE ; then 366 | exited_with_exitcode=$? 367 | else 368 | exited_with_exitcode=0 369 | fi 370 | TIME_ELAPSED=$(( SECONDS - TIME_STARTED )) 371 | 372 | log_debug "curl exited with $exited_with_exitcode, duration $TIME_ELAPSED seconds. Trying to stop capture..." 373 | 374 | if ! stop_capture_from_fritz ; then 375 | log_debug "Failed to stop remote capture after completion" 376 | # The first stop can fail if there is an existing session that cannot be canceled for some reason. 377 | # The second stop can fail if the existing session is still there and preventing us from capturing - user must be informed. 378 | # The second stop can also fail if the sid is invalid (logging out does not abort existing capture sessions). 379 | if [ -n "$failed_to_stop_capture_at_start" ] ; then 380 | echo "Failed to stop existing capture session. Verify that the capture is stopped at https://${FRITZ_BOX_IP}/?lp=cap" >&2 381 | fi 382 | fi 383 | 384 | if [ $exited_with_exitcode != 0 ] ; then 385 | log_debug "Exited with error ($exited_with_exitcode)" 386 | echo "Capture failed or was interrupted" >&2 387 | elif [[ $TIME_ELAPSED -ge 0 ]] && [[ $TIME_ELAPSED -lt 5 ]] ; then 388 | # Despite succesful exit, the capture ended early. Unlikely to be intentional. 389 | log_debug "Exited too soon ($TIME_ELAPSED)" 390 | echo "Capture ended suspiciously early." >&2 391 | echo "Is sid valid? Is capture supported by https://${FRITZ_BOX_IP}/?lp=cap ?" >&2 392 | exited_with_exitcode=1 393 | else 394 | log_debug "Exited normally" 395 | fi 396 | 397 | # Flush FIFO file if needed - otherwise Wireshark doesn't know that we're done. 398 | if [ $FIFO_FILE_CLOSED != 1 ] ; then 399 | [ -e "$FIFO_FILE" ] && echo >&3 400 | exec 3>&- # Close FIFO_FILE fd 401 | fi 402 | 403 | # TODO: Should this extcap script return a non-zero exit code? It's not really significant. 404 | exit $exited_with_exitcode 405 | --------------------------------------------------------------------------------