#!/bin/bash # # iplogfilter # # Beautify iptables, ipchains and ipfilter logs for display to improve # readability of the most important information. # A reasonably decent version of /bin/sh will be required (tested with bash 2); # bash 3+ should also work. # # Copyright (C) by Volker Kuhlmann # http://volker.top.geek.nz/contact.html # All rights reserved. # Released under the terms of the GNU General Public License (GPL) Version 2. # See http://www.gnu.org/ for details. # # Volker Kuhlmann # As ipf: # ...; 4 Sep 2000; 29 Jul 2001; 20 Feb; 28 Mar; 2 Apr 2002 # As iplogfilter: # 10, 11 Sep; 16 Dec 2002 # 15 Jan; 2 Feb; 26 Mar; 6, 23 May; 11 Jun; 19 Sep; 3 Oct 2003 # 29 Apr; 9 Aug 2004 # 3 May 2005 # 6 Feb; 3 Oct; 28 Dec 2006 # 5 Jun 2007 # 17 Mar; 28, 29 Apr; 28 Oct 2008 # 3.8, 25 Jan 2009 # 3.9, 19 Dec 2014 # Update to openSUSE 12.3 iptables format. # 3.9.2, 31 May 2016 # Deal with IN-ACC-EST. Shorten PROTO=ICMPv6. # chr0(){ echo @;};chr1(){ echo .;} VERSION="3.9.2, 31 May 2016" AUTHOR="Volker Kuhlmann " COPYRIGHT="Copyright (C) 2000-2016" #### #### Constants and initialised variables # logfiles=( # Log files to look for, in the order given here. /var/log/sys/kern /var/log/sys/kernel /var/log/facility/kern /var/log/firewall /var/log/messages # This probably always wants to be last. ) outfile="/root/t/ipf" #### #### Version, Usage, Help # show_version() { echo "${0##*/} version $VERSION $COPYRIGHT by $AUTHOR" } show_usage() { echo " Usage: ${0##*/} [OPTIONS] ARGS Version $VERSION $COPYRIGHT by $AUTHOR " } show_help() { show_usage echo "\ Pretty-print iptables, ipchains or pf logs to make the most important information easily readable. Reads log from the first of '${logfiles[@]}' which can be found, writes to stdout. ICMP lines are currently not formatted well. There should be a 'tail -f' option. OPTIONS: -h|--help Show help --version Show version --chains|-c Read ipchains format log file (default iptables) --packetfilter|-p Read BSD new pf format log file (default iptables) --packetfilter1|-p1 Read BSD old pf format log file (default iptables) --lines|-l NUM Examine only the last NUM lines of input --filter|-f Read stdin and write stdout --logfile|-i IN Read input from file IN. The file may be compressed and have one of the usual extensions. --outfile|-o OUT Save output to OUT --save|-s Save output to '$outfile' --logre RE Use RE to extract packet log lines, instead of a default. RE is also removed from line. RE is used with egrep and sed! This feature is experimental! " } Version() { show_version; exitwith ErrVersion; } Usage() { show_usage echo "$CALL_WITH_HELP_TEXT " exitwith ErrUsage } CALL_WITH_HELP_TEXT="Call with --help for help." Help() { test "$1" && exitwith ErrHelp show_help; show_help; exitwith ErrOK; } #### #### Error/Exit codes # exitwith() { exec 1>&2 # write stdout on stderr instead case "$1" in ErrOK|ErrVersion) exit 0;; ErrUsage|ErrHelp) # Output generated by function (program) $2, if given test -n "$2" && "$2" exit 1;; # more codes in here v ErrNoLogfile) echo "Can't open any of the log files: ${logfiles[@]}" exit 2;; ErrCreate) echo "Can't create: '$2'" exit 2;; # more codes in here ^ ErrBadoption) echo "Bad option '$2'." echo "Call with -h for help." exit 9;; ErrMissingParameter) echo "A required parameter for option $2 is missing." echo "Call with -h for help." exit 9;; *) echo "Internal error: exitwith() called with illegal error code '$1'." exit 19;; esac } #### #### Parse command line parameters # ## From functions_vk.bash (to make this standalone - good idea?) checkargmm() { test "$1" != "--" -a "$2" != "--" \ && test \( "${1:0:1}" != "-" -o "$1" = "-" \) \ -a \( "${2:0:1}" != "-" -o "$2" = "-" \) # uncomment after && above to allow arguments starting with "-" to options # (a single "-" is always allowed) #true # uncomment this to allow "--" as argument to an option } checkarg2() { # 1 required test $# -ge 2 && checkargmm "$2" && return exitwith ErrMissingParameter "$1" } ## parse_cmd_line() { local cmdline arg unset -v debug lines filter save logfile LOGLINE_RE filter1=ipt_filter1 filter2=ipt_filter2 test "$1" = "--debug" && debug=1 # pre-check for debug #set -- -opt1 "$@" # pre-init options while [ -n "$1" ]; do test $debug && echo "Current arg: $1" arg="${1#-}" test "$arg" == "$1" && break # stop if word does not start with "-" test "$arg" == "-" && { shift; break; } # stop if word is "--" # allow both "-long" and "--long" for long options: # (when commenting this out, must enter long options like "-longopt"!) #arg="${arg#-}" case "$arg" in d|-debug) debug=1;; h|help|-help) Help;; -usage) Usage;; -version) Version;; -chains|c) filter1=ipc_filter1 filter2=ipc_filter2 ;; -iptables) filter1=ipt_filter1 filter2=ipt_filter2 ;; -packetfilter1|p1) filter1=pf12_filter1 filter2=pf1_filter2 ;; -packetfilter|p) filter1=pf12_filter1 filter2=pf2_filter2 ;; -logre) checkarg2 "$@"; LOGLINE_RE="$2"; shift;; -lines|l) checkarg2 "$@"; lines="$2"; shift;; -filter|f) filter=1;; -save|s) save=1;; -logfile|-log|-in|i) checkarg2 "$@"; logfile="$2"; shift;; -outfile|-out|o) checkarg2 "$@"; outfile="$2"; save=1; shift;; "") break;; # allow "-" as file arg *) exitwith ErrBadoption "$1";; #*) break;; # stop option scanning with first unknown option esac shift done # a leftover numerical arg is taken as -l case "$1" in [0-9]*) test -z "$lines" && lines="$1"; shift;; esac } #### #### Filters for ipchains logs # ipc_filter1() { grep 'kernel: Packet log:' } ipc_filter2() { sed -e 's/[^ ]* kernel: Packet log: //' \ -e 's/input/in/; s/output/ou/; s/forward/fw/' \ -e 's/\(:..\) fw_masq/\1 mq/' \ -e 's/DENY/D/; s/ACCEPT/A/; s/REJECT/R/; s/MASQ/M/; s/fw_masq/m/' \ -e 's/PROTO=1 /icmp /; s/PROTO=6 /tcp /; s/PROTO=17 /udp /' \ -e 's/tcp \(.*\)SYN /tcpS \1/' \ -e 's/PROTO/P/; s/ P=[0-9] /& /' \ -e 's/L=.*[TO]=[0-9xa-fA-F]* //' \ -e 's/eth0/e0/; s/eth1/e1/; s/eth2/e2/; s/ppp0/p0/' \ -e 's/(#\([0-9]*\))/#\1/' \ | filter_month } #### #### Filters for iptables logs # ipt_filter1() { if [ -n "$LOGLINE_RE" ]; then egrep "$LOGLINE_RE" else egrep "(klogd|kernel): (\[[0-9.]+\] )?(SFW2-|SuSE-FW)" fi } ipt_filter2() { local sed_re if [ -n "$LOGLINE_RE" ]; then sed_re="s/$LOGLINE_RE//;" fi sed \ "$sed_re"' s/kernel: [][0-9.]* SFW2-//; s/kernel: SuSE-FW-//; s/kernel: SFW2-//; s/klogd: SFW2-//; s/MAC=[^ ]* //; s/LEN=[^ ]* *//g; s/TOS=[^ ]* //; s/PREC=[^ ]* //; s/TTL=[^ ]* //; s/ID=[^ ]* //g; s/CODE=[^ ]* //; s/DF //; s/WINDOW=[^ ]* //; s/RES=[^ ]* //; s/URGP=[^ ]* //; s/OPT ([0-9A-Za-z]*)//; # # These for SuSEfirewall2 3.2 (needs to be before shorter strings): # chains: s/IN-ACC-RELATED/ARL/; s/IN-ACC-ICMP/AI /; s/IN-ACC-TCP/AT /; s/IN-ACC-EST/AET/; s/IN-REJECT-ACK/R /; s/IN-[A-Za-z0-9]*/Dx /; s/OUT-[A-Za-z0-9]*/Ox /; s/FWD-ILL-ROUTING/FIR/; # $rule chains: s/INext-DROP-DEFLT-INV/DDI/; s/INext-DROP-DEFLT/DD /; s/INext-ACC-TCP/A /; s/INext-ACC-TRUST/AT /; s/[A-Z][A-Za-z]*-ACC-[A-Za-z0-9]*/Axx/; s/[A-Z][A-Za-z]*-ACC/Ax /; s/[A-Z][A-Za-z]*-DROP-[A-Za-z0-9]*/Dxx/; s/[A-Z][A-Za-z]*-DROP/Dx /; s/[A-Z][A-Za-z]*-FWD-[A-Za-z0-9]*/Fxx/; s/[A-Z][A-Za-z]*-FWD/Fx /; # End SuSEfirewall2 3.2 # s/ACCEPT-TRUST/AT /; s/ACCEPT-REVERSE_MASQ/ARM/; s/ACCEPT-MASQ/AM /; s/ACCEPT-ICMP/AIC/; s/ACCEPT-ALL-INTERNAL/AAI/; s/ACCEPT-PING/AP /; s/ACCEPT-REDIRECT/AR /; s/ACCEPT/A /; s/DROP-BCASTe/DBC/; s/DROP-DEFAULT/DD /; s/DROP-ANTI-SPOOFING/DAS/; s/DROP-ANTI-SPOOF/DAS/; s/DROP-CIRCUMVENTION/DC /; s/DROP/D /; s/UNAUTHORIZED-TARGET/UT /; s/ILLEGAL-TARGET/IT /; s/ILLEGAL-ROUTING/IR /; s/UNAUTHORIZED-ROUTING/UR /; s/TRACEROUTE-ATTEMPT/TA /; s/NO_ACCESS_INT->FWEXT/NIE/; s/ACCESS_DENIED_INT/NIE/; s/REJECT/R /; s/OUTPUT-ERROR/OE /; s/OUT-ERROR/OE /; s/REJ-IP/RIP/; s/IN=\(.\)[a-z]*\([0-9]\) /\1\2 /; s/IN= / /; s/ OUT=\(.\)[a-z]*\([0-9]\) /->\1\2 /; s/ OUT= /-> /; s/PROTO=TCP\(.*\) SYN /PROTO=TCPS \1/; s/PROTO=ICMPv6 /PROTO=ICM6 /; s/SRC=\([^[]*\) PROTO=\([^ ]*\) */=\2 SRC=\1 /; s/ =\([^ ]\) / \1 /; s/ =\([^ ][^ ]\) / \1 /; s/ =\([^ ][^ ][^ ]\) / \1 /; s/ =\([^ ]*\) / \1 /; s/SRC=\([^ ]*\)\([^[]*\) SPT=\([^ ]*\)/\1:\3->\2/; s/SRC=\([^ ]*\)\(.*\)/\1->\2/; s/ DST=\([^ ]*\)\([^[]*\) DPT=\([^ ]*\)/\1:\3\2/; s/ DST=\([^ ]*\)\(.*\)/\1\2/; s/ *$//; ' \ | filter_month } ipt_filter2_awk() { awk '{ sub("kernel: SuSE-FW-", "") sub("kernel: SFW2-", "") print } { fflush("") } ' \ | filter_month } #### #### Filters for BSD pf (packet filter) logs. # # Match pf log lines. pf12_filter1() { if [ -n "$LOGLINE_RE" ]; then egrep "$LOGLINE_RE" else egrep " pf: [0-9]" fi } # Old pf log format. pf1_filter2() { local sed_re if [ -n "$LOGLINE_RE" ]; then sed_re="s/$LOGLINE_RE//;" fi sed \ "$sed_re"' s/pf: [0-9. ]* rule // s/[0-9][^ ]*): *// s/ next-header: Options \(.*icmp6\)/ proto: ICMP \1/ s/ next-header: / proto: / s/: (.*proto: \([^ ]*\)/ \1: (/ s/: (.*length: [0-9]*, options [^.]*) / / s/: (.*length: [0-9]*) / / #s/\.\([0-9]*\) \(.\) \([0-9.]*\)\.\([0-9]*\)[^0-9 ]*.*/:\1-\2\3:\4 / s/ \([0-9]*\.[0-9]*\.[0-9]*\.[0-9]*\)\.\([0-9]*\) */ \1:\2 /g s/ > /->/ s/ *: .*$// s/ pass in on \([^ ]*\) / A \1-> / s/ block in on \([^ ]*\) / D \1-> / s/ TCP */TCP / s/ UDP */UDP / s/ IGMP */IGMP / s/ ICMPv6 / ICMP / s/ ICMP */ICMP / ' \ | filter_month } # New pf log format (pfSense 1.2). # Some pre-release versions do not contain the IP protocol. pf2_filter2() { local sed_re if [ -n "$LOGLINE_RE" ]; then sed_re="s/$LOGLINE_RE//;" fi sed \ "$sed_re"' #i - - - ---------- # delete from "pf:" to "rule "; 2nd line makes it more robust with LOGLINE_RE s/pf: [0-9. ]* rule *// s/ *[0-9. ]* rule */ / # delete from digit to "):" following match/block s/[0-9][^ ]*): *// # Add colon again after proto (missing with pfSense 1.2.2), to keep the # rules below working. s/proto /proto: / # if no protocol, insert "---" /proto:/! s/\( [^ ]* in on [^ ]* \)/\1(proto: --- / # Remove crud following proto: XYZ to closing ")". # (pfSense 1.2.2: colon missing after "length".) s/proto: *\([^ ]*\) .*length.*options.*)\( *[0-9]*\.[0-9]*\.\)/proto: \1\2/ s/proto: *\([^ ]*\) .*length:* *[0-9]*)/proto: \1/ # Try for protocol fixed-width s/proto: \([^ ][^ ][^ ]\) /proto: \1 / # remove crud following interface up to incl "proto:" s/\( on [^ ]*:\) *(.*proto: */\1 / # Replace "pass in" and "block in" with "A->" and "D->" # (analogous to iptables accept/drop). s/ pass in on \([^ ][^ ][^ ][^ ]\): */ A \1-> / s/ pass in on \([^ ][^ ][^ ]\): */ A \1-> / s/ block in on \([^ ][^ ][^ ][^ ]\): */ D \1-> / s/ block in on \([^ ][^ ][^ ]\): */ D \1-> / s/ block out on \([^ ][^ ][^ ][^ ]\): */ D ->\1 / s/ block out on \([^ ][^ ][^ ]\): */ D ->\1 / # arrow between src and dst IP s/ > /->/ # keep first word after the colon following the dst IP s/\(\.[0-9]*\.[0-9]*\.[0-9]*\): *\([^ ,]*\).*$/\1 \2/ # deal with trailing "339+[|domain]" s/]$// s/ [^ ]*[^ a-zA-Z0-9_]\([^ ]*\)$/ \1/ # separate the port number with a colon, not a period s/\([0-9]*\.[0-9]*\.[0-9]*\.[0-9]*\)\.\([0-9]*\)/\1:\2/g # minor tidy-up s/ S$/ TCPS/ ' \ | filter_month } #### #### Functions # # Translate a 3-letter month name into a 2-digit number, and write month and day # in the format MMDD. # stdin -> stdout filter_month() { awk ' BEGIN {months = "JanFebMarAprMayJunJulAugSepOctNovDec"} /^....-..-..T/ { print substr($0, 6, 2) substr($0, 9, 2) "-" substr($0, 12, 8) \ substr($0, index($0, " ")) next } {month = $1; printf ("%02d%02d-%s\n", (index (months, month) + 2) / 3, $2, substr ($0, 8))} #gawk only { fflush("") } ' # printf ("%02d%02d%s\n", # (index (months, month) + 2) / 3, $2, substr ($0, 7))} } # Alternativ format, not currently used. filter_month_2() { awk ' BEGIN {months = "JanFebMarAprMayJunJulAugSepOctNovDec"} {month = $1; sub (":", "", $0) sub (":", "", $0) print printf ("%02d%02dT%s\n", (index (months, month) + 2) / 3, $2, substr ($0, 8) )} ' } # Only use the last $lines lines of the input. # stdin -> stdout limit_input_last() { tail -"$lines" | $oldfilter } # Unless a logfile is already known (specified on the command line), search # for the first one of $logfiles[] which can be found, and return it in # $logfile. find_logfile() { test -n "$logfile" && return local file for file in "${logfiles[@]}"; do if [ -r "$file" ]; then logfile="$file" return fi done exitwith ErrNoLogfile } # Open the given logfile, which exists. # The logfile is given by name, and may be compressed. # The decompressed logfile can be read from &3 (file descriptor) after function # exit. open_logfile() { case "$logfile" in *.gz|*.Z|*.z) exec 3< <( gunzip < "$logfile" );; *.bz2) exec 3< <( bunzip2 < "$logfile" );; *.lzma) exec 3< <( lzma -d < "$logfile" );; *.xz) exec 3< <( xz -d < "$logfile" );; *) exec 3<"$logfile";; esac } #### #### Main # parse_cmd_line "$@" if [ -n "$lines" ]; then oldfilter=$filter1 filter1=limit_input_last fi if [ -n "$filter" ]; then $filter1 | $filter2 elif [ -n "$save" ]; then find_logfile umask 77 mkdir -p "`dirname $outfile`" || exitwith ErrCreate "`dirname $outfile`" open_logfile $filter1 <&3 | $filter2 > "$outfile" else find_logfile open_logfile $filter1 <&3 | $filter2 fi