mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-10 18:34:18 +08:00
899 lines
24 KiB
Bash
Executable File
899 lines
24 KiB
Bash
Executable File
#!/bin/sh
|
|
|
|
SCRIPT_NAME="${0##*/}"
|
|
|
|
TIMEOUT=8
|
|
CLIENT_IP=
|
|
METHOD=HEAD
|
|
KEEP_TMP=0
|
|
VERBOSE=0
|
|
ENABLE_TCPDUMP=0
|
|
TCPDUMP_IFACE=any
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage:
|
|
$SCRIPT_NAME [-t seconds] [-X HEAD|GET] [-p] [-i iface] [-v] [-K] <url>
|
|
$SCRIPT_NAME -c <client_ip> [-t seconds] [-p] [-i iface] [-v] [-K] <url>
|
|
|
|
Modes:
|
|
default Run curl on this router and trace hijpass out-route.
|
|
-c client_ip Trace hijpass pre-filter for a real request from the client.
|
|
|
|
Examples:
|
|
$SCRIPT_NAME https://www.gstatic.com/generate_204
|
|
$SCRIPT_NAME -p https://www.gstatic.com/generate_204
|
|
$SCRIPT_NAME -p -i pppoe-wan https://www.gstatic.com/generate_204
|
|
$SCRIPT_NAME -c 192.168.1.100 https://www.gstatic.com/generate_204
|
|
|
|
Notes:
|
|
The default request is TCP HEAD. For transparent LAN traffic, use -c and
|
|
run a matching curl request from the client during the trace window.
|
|
Use -p to enable tcpdump packet capture. The default tcpdump interface is any.
|
|
Use -v to print raw trace and log lines.
|
|
EOF
|
|
}
|
|
|
|
die() {
|
|
printf 'error: %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
has_cmd() {
|
|
command -v "$1" >/dev/null 2>&1
|
|
}
|
|
|
|
is_number() {
|
|
case "$1" in
|
|
''|*[!0-9]*) return 1 ;;
|
|
*) return 0 ;;
|
|
esac
|
|
}
|
|
|
|
is_port() {
|
|
is_number "$1" || return 1
|
|
[ "$1" -ge 1 ] 2>/dev/null && [ "$1" -le 65535 ] 2>/dev/null
|
|
}
|
|
|
|
is_ipv4() {
|
|
printf '%s\n' "$1" | awk -F. '
|
|
NF != 4 { exit 1 }
|
|
{
|
|
for (i = 1; i <= 4; i++) {
|
|
if ($i !~ /^[0-9]+$/ || $i < 0 || $i > 255) {
|
|
exit 1
|
|
}
|
|
}
|
|
}
|
|
'
|
|
}
|
|
|
|
is_ipv6() {
|
|
case "$1" in
|
|
*:*) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
ip_family() {
|
|
if is_ipv4 "$1"; then
|
|
printf '4\n'
|
|
elif is_ipv6 "$1"; then
|
|
printf '6\n'
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
upper() {
|
|
printf '%s' "$1" | tr '[:lower:]' '[:upper:]'
|
|
}
|
|
|
|
while getopts 'c:t:X:i:Kpvh' opt; do
|
|
case "$opt" in
|
|
c) CLIENT_IP="$OPTARG" ;;
|
|
t) TIMEOUT="$OPTARG" ;;
|
|
X) METHOD="$(upper "$OPTARG")" ;;
|
|
i) TCPDUMP_IFACE="$OPTARG" ;;
|
|
K) KEEP_TMP=1 ;;
|
|
p) ENABLE_TCPDUMP=1 ;;
|
|
v) VERBOSE=1 ;;
|
|
h)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
usage >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
done
|
|
shift $((OPTIND - 1))
|
|
|
|
[ "$#" -eq 1 ] || {
|
|
usage >&2
|
|
exit 2
|
|
}
|
|
|
|
URL="$1"
|
|
MODE=router
|
|
[ -n "$CLIENT_IP" ] && MODE=client
|
|
|
|
case "$METHOD" in
|
|
HEAD|GET) ;;
|
|
*) die "method must be HEAD or GET" ;;
|
|
esac
|
|
|
|
is_number "$TIMEOUT" || die "timeout must be an integer"
|
|
[ "$TIMEOUT" -ge 1 ] 2>/dev/null || die "timeout must be >= 1"
|
|
|
|
if [ -n "$CLIENT_IP" ]; then
|
|
ip_family "$CLIENT_IP" >/dev/null || die "invalid client IP: $CLIENT_IP"
|
|
fi
|
|
case "$TCPDUMP_IFACE" in
|
|
''|*[!A-Za-z0-9_.:@-]*)
|
|
die "invalid tcpdump interface: $TCPDUMP_IFACE"
|
|
;;
|
|
esac
|
|
|
|
has_cmd awk || die "awk is required"
|
|
has_cmd curl || die "curl is required"
|
|
has_cmd nft || die "nft is required"
|
|
if [ "$ENABLE_TCPDUMP" -eq 1 ]; then
|
|
has_cmd tcpdump || die "tcpdump is required when -p is used"
|
|
fi
|
|
|
|
[ "$(id -u)" = "0" ] || die "must run as root"
|
|
nft list table inet hijpass >/dev/null 2>&1 || die "nft table inet hijpass not found"
|
|
|
|
parse_url() {
|
|
case "$URL" in
|
|
https://*)
|
|
SCHEME=https
|
|
PORT=443
|
|
REST="${URL#https://}"
|
|
;;
|
|
http://*)
|
|
SCHEME=http
|
|
PORT=80
|
|
REST="${URL#http://}"
|
|
;;
|
|
*)
|
|
die "only http:// and https:// URLs are supported"
|
|
;;
|
|
esac
|
|
|
|
HOSTPORT="${REST%%/*}"
|
|
[ -n "$HOSTPORT" ] || die "URL host is empty"
|
|
|
|
case "$HOSTPORT" in
|
|
\[*\]*)
|
|
HOST="${HOSTPORT#\[}"
|
|
HOST="${HOST%%\]*}"
|
|
AFTER_BRACKET="${HOSTPORT#*\]}"
|
|
case "$AFTER_BRACKET" in
|
|
:*) PORT="${AFTER_BRACKET#:}" ;;
|
|
'') ;;
|
|
*) die "invalid IPv6 host/port: $HOSTPORT" ;;
|
|
esac
|
|
;;
|
|
*:*)
|
|
LAST_PART="${HOSTPORT##*:}"
|
|
PREFIX_PART="${HOSTPORT%:*}"
|
|
if is_number "$LAST_PART"; then
|
|
case "$PREFIX_PART" in
|
|
*:*) HOST="$HOSTPORT" ;;
|
|
*)
|
|
HOST="$PREFIX_PART"
|
|
PORT="$LAST_PART"
|
|
;;
|
|
esac
|
|
else
|
|
HOST="$HOSTPORT"
|
|
fi
|
|
;;
|
|
*)
|
|
HOST="$HOSTPORT"
|
|
;;
|
|
esac
|
|
|
|
[ -n "$HOST" ] || die "URL host is empty"
|
|
case "$HOST" in
|
|
*[!A-Za-z0-9_.:-]*)
|
|
die "unsupported URL host: $HOST"
|
|
;;
|
|
esac
|
|
is_port "$PORT" || die "invalid URL port: $PORT"
|
|
}
|
|
|
|
resolve_host() {
|
|
if is_ipv4 "$HOST" || is_ipv6 "$HOST"; then
|
|
printf '%s\n' "$HOST"
|
|
return 0
|
|
fi
|
|
|
|
has_cmd nslookup || return 0
|
|
|
|
nslookup "$HOST" 2>/dev/null | awk '
|
|
/^Name:/ { answer = 1; next }
|
|
answer && /^Address[[:space:]][0-9]+:/ { print $3; next }
|
|
answer && /^Address:/ { print $2; next }
|
|
' | sed 's/%.*//' | awk '
|
|
index($0, ".") || index($0, ":") {
|
|
if (!seen[$0]++) print
|
|
}
|
|
'
|
|
}
|
|
|
|
parse_url
|
|
|
|
TARGET_IPS=
|
|
|
|
TMP_DIR="${TMPDIR:-/tmp}/hijpass-trace.$$"
|
|
NFT_LOG="$TMP_DIR/nft.trace"
|
|
NFT_SETUP_LOG="$TMP_DIR/nft.setup"
|
|
SYS_LOG="$TMP_DIR/system.log"
|
|
CURL_OUT="$TMP_DIR/curl.out"
|
|
CURL_ERR="$TMP_DIR/curl.err"
|
|
CURL_HDR="$TMP_DIR/curl.headers"
|
|
LOG_INDEX="$TMP_DIR/log-files.index"
|
|
SERVICE_LOG="$TMP_DIR/service.log"
|
|
TCPDUMP_LOG="$TMP_DIR/tcpdump.log"
|
|
TCPDUMP_ERR="$TMP_DIR/tcpdump.err"
|
|
TCPDUMP_FILTER=
|
|
TRACE_COMMENT="hijpass-trace-$$"
|
|
NFT_PID=
|
|
LOG_PID=
|
|
TCPDUMP_PID=
|
|
CLEANED=0
|
|
|
|
mkdir -p "$TMP_DIR" || die "failed to create $TMP_DIR"
|
|
: >"$NFT_LOG"
|
|
: >"$NFT_SETUP_LOG"
|
|
: >"$SYS_LOG"
|
|
: >"$CURL_OUT"
|
|
: >"$CURL_ERR"
|
|
: >"$CURL_HDR"
|
|
: >"$LOG_INDEX"
|
|
: >"$SERVICE_LOG"
|
|
: >"$TCPDUMP_LOG"
|
|
: >"$TCPDUMP_ERR"
|
|
|
|
delete_trace_rules() {
|
|
for chain in pre-filter out-route; do
|
|
nft -a list chain inet hijpass "$chain" 2>/dev/null |
|
|
awk -v c="$TRACE_COMMENT" '
|
|
index($0, "comment \"" c "\"") {
|
|
for (i = 1; i <= NF; i++) {
|
|
if ($i == "handle") print $(i + 1)
|
|
}
|
|
}
|
|
' |
|
|
while read -r handle; do
|
|
[ -n "$handle" ] || continue
|
|
nft delete rule inet hijpass "$chain" handle "$handle" >/dev/null 2>&1
|
|
done
|
|
done
|
|
}
|
|
|
|
cleanup() {
|
|
[ "$CLEANED" -eq 1 ] && return
|
|
CLEANED=1
|
|
|
|
[ -n "$NFT_PID" ] && kill "$NFT_PID" >/dev/null 2>&1
|
|
[ -n "$LOG_PID" ] && kill "$LOG_PID" >/dev/null 2>&1
|
|
[ -n "$TCPDUMP_PID" ] && kill "$TCPDUMP_PID" >/dev/null 2>&1
|
|
[ -n "$NFT_PID" ] && wait "$NFT_PID" >/dev/null 2>&1
|
|
[ -n "$LOG_PID" ] && wait "$LOG_PID" >/dev/null 2>&1
|
|
[ -n "$TCPDUMP_PID" ] && wait "$TCPDUMP_PID" >/dev/null 2>&1
|
|
|
|
delete_trace_rules
|
|
|
|
if [ "$KEEP_TMP" -eq 0 ]; then
|
|
rm -rf "$TMP_DIR"
|
|
else
|
|
printf '\nTemporary files kept: %s\n' "$TMP_DIR"
|
|
fi
|
|
}
|
|
|
|
trap cleanup EXIT
|
|
trap 'cleanup; exit 130' INT
|
|
trap 'cleanup; exit 143' TERM
|
|
|
|
add_nft_rule() {
|
|
chain="$1"
|
|
shift
|
|
nft insert rule inet hijpass "$chain" "$@" meta nftrace set 1 comment "$TRACE_COMMENT" \
|
|
>>"$NFT_SETUP_LOG" 2>&1
|
|
}
|
|
|
|
target_expr_family() {
|
|
family="$(ip_family "$1" 2>/dev/null || true)"
|
|
case "$family" in
|
|
4) printf 'ip daddr\n' ;;
|
|
6) printf 'ip6 daddr\n' ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
client_expr_family() {
|
|
family="$(ip_family "$1" 2>/dev/null || true)"
|
|
case "$family" in
|
|
4) printf 'ip saddr\n' ;;
|
|
6) printf 'ip6 saddr\n' ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
add_router_trace_rules() {
|
|
for target_ip in $TARGET_IPS; do
|
|
expr="$(target_expr_family "$target_ip" || true)"
|
|
[ -n "$expr" ] || continue
|
|
set -- $expr "$target_ip" tcp dport "$PORT"
|
|
add_nft_rule out-route "$@"
|
|
done
|
|
|
|
# Also trace the router-local destination port. Curl may use an address
|
|
# family different from the router-side nslookup result.
|
|
add_nft_rule out-route tcp dport "$PORT" || return 1
|
|
}
|
|
|
|
add_client_trace_rules() {
|
|
client_expr="$(client_expr_family "$CLIENT_IP" || true)"
|
|
[ -n "$client_expr" ] || return 1
|
|
set -- $client_expr "$CLIENT_IP" tcp dport "$PORT"
|
|
|
|
# Keep one broad client+port rule so the trace still works when the
|
|
# client resolves the host to a different address than the router.
|
|
add_nft_rule pre-filter "$@" || return 1
|
|
}
|
|
|
|
collect_log_paths() {
|
|
has_cmd uci || return 0
|
|
|
|
uci -q show hijpass 2>/dev/null |
|
|
sed -n "s/^hijpass\\..*\\.log_path='\\([^']*\\)'$/\\1/p; s/^hijpass\\..*\\.log_path=\\([^']*\\)$/\\1/p" |
|
|
awk 'NF && !seen[$0]++'
|
|
}
|
|
|
|
snapshot_log_positions() {
|
|
collect_log_paths | while read -r log_path; do
|
|
[ -f "$log_path" ] || continue
|
|
line_count="$(wc -l <"$log_path" 2>/dev/null | tr -d ' ')"
|
|
is_number "$line_count" || line_count=0
|
|
printf '%s %s\n' "$line_count" "$log_path" >>"$LOG_INDEX"
|
|
done
|
|
}
|
|
|
|
collect_new_service_logs() {
|
|
: >"$SERVICE_LOG"
|
|
while read -r line_count log_path; do
|
|
[ -n "$line_count" ] || continue
|
|
[ -f "$log_path" ] || continue
|
|
start_line=$((line_count + 1))
|
|
{
|
|
printf -- '--- %s ---\n' "$log_path"
|
|
tail -n +"$start_line" "$log_path" 2>/dev/null
|
|
} >>"$SERVICE_LOG"
|
|
done <"$LOG_INDEX"
|
|
}
|
|
|
|
start_monitors() {
|
|
nft monitor trace >"$NFT_LOG" 2>&1 &
|
|
NFT_PID="$!"
|
|
|
|
if has_cmd logread; then
|
|
logread -f >"$SYS_LOG" 2>&1 &
|
|
LOG_PID="$!"
|
|
fi
|
|
}
|
|
|
|
stop_monitors() {
|
|
[ -n "$NFT_PID" ] && kill "$NFT_PID" >/dev/null 2>&1
|
|
[ -n "$LOG_PID" ] && kill "$LOG_PID" >/dev/null 2>&1
|
|
[ -n "$NFT_PID" ] && wait "$NFT_PID" >/dev/null 2>&1
|
|
[ -n "$LOG_PID" ] && wait "$LOG_PID" >/dev/null 2>&1
|
|
NFT_PID=
|
|
LOG_PID=
|
|
}
|
|
|
|
tcpdump_filter() {
|
|
# Keep capture broad enough to include proxy outbound traffic using the
|
|
# same destination port. Output is filtered later to keep the report short.
|
|
printf 'tcp and port %s\n' "$PORT"
|
|
}
|
|
|
|
start_tcpdump() {
|
|
[ "$ENABLE_TCPDUMP" -eq 1 ] || return 0
|
|
|
|
TCPDUMP_FILTER="$(tcpdump_filter)"
|
|
tcpdump -p -i "$TCPDUMP_IFACE" -nn -tt -l -s 96 "$TCPDUMP_FILTER" \
|
|
>"$TCPDUMP_LOG" 2>"$TCPDUMP_ERR" &
|
|
TCPDUMP_PID="$!"
|
|
|
|
sleep 1
|
|
if ! kill -0 "$TCPDUMP_PID" >/dev/null 2>&1; then
|
|
wait "$TCPDUMP_PID" >/dev/null 2>&1
|
|
TCPDUMP_PID=
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
stop_tcpdump() {
|
|
[ -n "$TCPDUMP_PID" ] || return 0
|
|
kill "$TCPDUMP_PID" >/dev/null 2>&1
|
|
wait "$TCPDUMP_PID" >/dev/null 2>&1
|
|
TCPDUMP_PID=
|
|
}
|
|
|
|
run_router_curl() {
|
|
if [ "$METHOD" = "HEAD" ]; then
|
|
curl -I -L -k -sS -o /dev/null -D "$CURL_HDR" \
|
|
--connect-timeout "$TIMEOUT" \
|
|
--max-time "$TIMEOUT" \
|
|
-w 'url_effective=%{url_effective}
|
|
http_code=%{http_code}
|
|
remote_ip=%{remote_ip}
|
|
remote_port=%{remote_port}
|
|
time_namelookup=%{time_namelookup}
|
|
time_connect=%{time_connect}
|
|
time_appconnect=%{time_appconnect}
|
|
time_starttransfer=%{time_starttransfer}
|
|
time_total=%{time_total}
|
|
' "$URL" >"$CURL_OUT" 2>"$CURL_ERR"
|
|
else
|
|
curl -L -k -sS -o /dev/null -D "$CURL_HDR" \
|
|
--connect-timeout "$TIMEOUT" \
|
|
--max-time "$TIMEOUT" \
|
|
-w 'url_effective=%{url_effective}
|
|
http_code=%{http_code}
|
|
remote_ip=%{remote_ip}
|
|
remote_port=%{remote_port}
|
|
time_namelookup=%{time_namelookup}
|
|
time_connect=%{time_connect}
|
|
time_appconnect=%{time_appconnect}
|
|
time_starttransfer=%{time_starttransfer}
|
|
time_total=%{time_total}
|
|
' "$URL" >"$CURL_OUT" 2>"$CURL_ERR"
|
|
fi
|
|
printf '%s\n' "$?" >"$TMP_DIR/curl.exit"
|
|
}
|
|
|
|
wait_for_client_request() {
|
|
printf 'Trace is active for %s seconds.\n' "$TIMEOUT"
|
|
printf 'Run this from client %s now:\n' "$CLIENT_IP"
|
|
printf ' curl -I -k %s\n' "$URL"
|
|
sleep "$TIMEOUT"
|
|
}
|
|
|
|
print_section() {
|
|
printf '\n== %s ==\n' "$1"
|
|
}
|
|
|
|
get_curl_value() {
|
|
awk -F= -v key="$1" '$1 == key { print substr($0, length(key) + 2) }' "$CURL_OUT" 2>/dev/null | tail -n 1
|
|
}
|
|
|
|
format_target_ips() {
|
|
if [ -z "$TARGET_IPS" ]; then
|
|
printf 'unavailable'
|
|
return
|
|
fi
|
|
|
|
printf '%s\n' "$TARGET_IPS" | awk '
|
|
NF {
|
|
count++
|
|
if (count <= 6) {
|
|
out = out ? out ", " $0 : $0
|
|
}
|
|
}
|
|
END {
|
|
if (count > 6) {
|
|
out = out ", +" (count - 6) " more"
|
|
}
|
|
print out
|
|
}
|
|
'
|
|
}
|
|
|
|
limit_unique_lines() {
|
|
awk -v max="$1" 'NF && !seen[$0]++ { print; if (++count >= max) exit }'
|
|
}
|
|
|
|
print_request_summary() {
|
|
print_section "Trace summary"
|
|
if [ "$MODE" = "client" ]; then
|
|
printf 'mode: client pre-filter, client=%s\n' "$CLIENT_IP"
|
|
else
|
|
printf 'mode: router out-route\n'
|
|
fi
|
|
printf 'request: %s %s\n' "$METHOD" "$URL"
|
|
printf 'target: %s:%s (%s)\n' "$HOST" "$PORT" "$(format_target_ips)"
|
|
|
|
if [ "$MODE" = "client" ]; then
|
|
printf 'curl: not run by this script in client mode\n'
|
|
return
|
|
fi
|
|
|
|
curl_exit="$(cat "$TMP_DIR/curl.exit" 2>/dev/null || printf '?')"
|
|
http_code="$(get_curl_value http_code)"
|
|
remote_ip="$(get_curl_value remote_ip)"
|
|
remote_port="$(get_curl_value remote_port)"
|
|
total_time="$(get_curl_value time_total)"
|
|
|
|
[ -n "$http_code" ] || http_code="-"
|
|
[ -n "$remote_ip" ] || remote_ip="-"
|
|
[ -n "$remote_port" ] || remote_port="-"
|
|
[ -n "$total_time" ] || total_time="-"
|
|
|
|
printf 'curl: exit=%s http=%s remote=%s:%s total=%ss\n' \
|
|
"$curl_exit" "$http_code" "$remote_ip" "$remote_port" "$total_time"
|
|
|
|
if [ -s "$CURL_ERR" ]; then
|
|
printf 'curl_error:\n'
|
|
sed -n '1,4p' "$CURL_ERR" | sed 's/^/ /'
|
|
fi
|
|
}
|
|
|
|
firewall_decision() {
|
|
shunt_port="$(uci -q get hijpass.@firewall[0].shunt_port 2>/dev/null || true)"
|
|
proxy_port="$(uci -q get hijpass.@firewall[0].proxy_port 2>/dev/null || true)"
|
|
|
|
if grep -q ' reject' "$NFT_LOG"; then
|
|
printf 'rejected by proxy-port-filter or another firewall rule\n'
|
|
elif [ -n "$proxy_port" ] && grep -q "tproxy to :$proxy_port" "$NFT_LOG"; then
|
|
printf 'tproxy to proxy port %s\n' "$proxy_port"
|
|
elif [ -n "$shunt_port" ] && grep -q "tproxy to :$shunt_port" "$NFT_LOG"; then
|
|
printf 'tproxy to shunt port %s\n' "$shunt_port"
|
|
elif grep -q 'ct mark set 0' "$NFT_LOG"; then
|
|
printf 'bypassed by proxy-port-filter or direct rule\n'
|
|
elif grep -q 'comment "shunt mark"' "$NFT_LOG"; then
|
|
printf 'marked for shunt, but final tproxy was not observed\n'
|
|
else
|
|
printf 'unknown from captured trace\n'
|
|
fi
|
|
}
|
|
|
|
trace_path() {
|
|
awk '
|
|
/inet hijpass/ {
|
|
for (i = 1; i <= NF - 2; i++) {
|
|
if ($i == "inet" && $(i + 1) == "hijpass") {
|
|
chain = $(i + 2)
|
|
if (!seen[chain]++) {
|
|
path = path ? path " -> " chain : chain
|
|
}
|
|
}
|
|
}
|
|
}
|
|
END {
|
|
print path ? path : "unavailable"
|
|
}
|
|
' "$NFT_LOG"
|
|
}
|
|
|
|
print_trace_steps() {
|
|
awk -v c="$TRACE_COMMENT" '
|
|
index($0, c) { next }
|
|
/inet hijpass/ && / rule / &&
|
|
($0 ~ /jump|return|tproxy|reject|accept|drop|ct mark set|meta mark set/) {
|
|
line = $0
|
|
sub(/^.*inet hijpass /, "", line)
|
|
gsub(/ counter packets [0-9]+ bytes [0-9]+/, "", line)
|
|
gsub(/ counter/, "", line)
|
|
sub(/ \(verdict.*$/, "", line)
|
|
gsub(/[[:space:]]+/, " ", line)
|
|
if (!seen[line]++) {
|
|
count++
|
|
if (length(line) > 180) {
|
|
line = substr(line, 1, 177) "..."
|
|
}
|
|
printf " %d. %s\n", count, line
|
|
if (count >= 14) exit
|
|
}
|
|
}
|
|
END {
|
|
if (count == 0) print " none"
|
|
}
|
|
' "$NFT_LOG"
|
|
}
|
|
|
|
print_nft_summary() {
|
|
print_section "Firewall"
|
|
|
|
if [ -s "$NFT_SETUP_LOG" ]; then
|
|
printf 'setup_warnings:\n'
|
|
sed 's/^/ /' "$NFT_SETUP_LOG"
|
|
fi
|
|
|
|
line_count="$(wc -l <"$NFT_LOG" 2>/dev/null | tr -d ' ')"
|
|
is_number "$line_count" || line_count=0
|
|
printf 'captured: %s nft trace lines\n' "$line_count"
|
|
|
|
if [ "$line_count" -eq 0 ]; then
|
|
printf 'decision: no nft trace captured\n'
|
|
printf 'check:\n'
|
|
printf ' - request did not match TCP port %s\n' "$PORT"
|
|
printf ' - client mode was used but no matching client request arrived\n'
|
|
printf ' - traffic was not handled by table inet hijpass\n'
|
|
return
|
|
fi
|
|
|
|
printf 'decision: %s\n' "$(firewall_decision)"
|
|
printf 'path: %s\n' "$(trace_path)"
|
|
printf 'key_rules:\n'
|
|
print_trace_steps
|
|
|
|
if [ "$VERBOSE" -eq 1 ]; then
|
|
printf 'raw_trace_tail:\n'
|
|
grep -E 'trace id| rule | verdict |tproxy|mark|jump|return|accept|reject|drop' "$NFT_LOG" 2>/dev/null |
|
|
tail -n 80 |
|
|
sed 's/^/ /'
|
|
fi
|
|
}
|
|
|
|
remote_ip_from_curl() {
|
|
get_curl_value remote_ip
|
|
}
|
|
|
|
tcpdump_related_lines() {
|
|
[ -s "$TCPDUMP_LOG" ] || return 1
|
|
|
|
remote_ip="$(remote_ip_from_curl)"
|
|
{
|
|
for target_ip in $TARGET_IPS $remote_ip; do
|
|
[ -n "$target_ip" ] && grep -F "$target_ip" "$TCPDUMP_LOG" 2>/dev/null
|
|
done
|
|
} | limit_unique_lines 200
|
|
}
|
|
|
|
tcpdump_clean_errors() {
|
|
[ -s "$TCPDUMP_ERR" ] || return 1
|
|
grep -Eiv 'listening on|verbose output suppressed|packets captured|packets received|packets dropped|promiscuous mode|doesn.t support promiscuous' \
|
|
"$TCPDUMP_ERR" 2>/dev/null
|
|
}
|
|
|
|
print_tcpdump_line_summary() {
|
|
awk '
|
|
NF {
|
|
iface = $2
|
|
dir = $3
|
|
proto = $4
|
|
src = $5
|
|
dst = $7
|
|
|
|
if (proto != "IP" && proto != "IP6") {
|
|
iface = "-"
|
|
dir = "-"
|
|
src = $3
|
|
dst = $5
|
|
}
|
|
|
|
sub(/:$/, "", dst)
|
|
|
|
iface_key = iface "/" dir
|
|
iface_count[iface_key]++
|
|
if (iface == "lo") lo_count++
|
|
|
|
flow = src " -> " dst
|
|
flow_count[flow]++
|
|
|
|
if ($0 ~ /Flags \[S\]/) syn++
|
|
if ($0 ~ /Flags \[S\.\]/) syn_ack++
|
|
if ($0 ~ /Flags \[F/) fin++
|
|
if ($0 ~ /Flags \[R/) rst++
|
|
if (match($0, /length [0-9]+/)) {
|
|
len = substr($0, RSTART + 7, RLENGTH - 7) + 0
|
|
if (len > 0) {
|
|
payload_packets++
|
|
payload_bytes += len
|
|
}
|
|
}
|
|
}
|
|
END {
|
|
if (NR == 0) {
|
|
print "target_summary: none"
|
|
exit
|
|
}
|
|
|
|
printf "target_summary:\n"
|
|
printf " tcp: syn=%d syn_ack=%d fin=%d rst=%d payload_packets=%d payload_bytes=%d\n", syn + 0, syn_ack + 0, fin + 0, rst + 0, payload_packets + 0, payload_bytes + 0
|
|
|
|
printf " interfaces:\n"
|
|
for (key in iface_count) {
|
|
printf " - %s packets=%d\n", key, iface_count[key]
|
|
}
|
|
|
|
printf " flows:\n"
|
|
shown = 0
|
|
for (key in flow_count) {
|
|
printf " - %s packets=%d\n", key, flow_count[key]
|
|
shown++
|
|
if (shown >= 6) break
|
|
}
|
|
|
|
if (lo_count == NR) {
|
|
print " note: all related packets were observed on lo; this is expected for tproxy loopback interception"
|
|
}
|
|
}
|
|
'
|
|
}
|
|
|
|
print_tcpdump_summary() {
|
|
[ "$ENABLE_TCPDUMP" -eq 1 ] || return 0
|
|
|
|
print_section "Packet capture"
|
|
printf 'interface: %s\n' "$TCPDUMP_IFACE"
|
|
printf 'filter: %s\n' "$TCPDUMP_FILTER"
|
|
|
|
tcpdump_errors="$(tcpdump_clean_errors)"
|
|
if [ -n "$tcpdump_errors" ]; then
|
|
printf 'tcpdump_warnings:\n'
|
|
printf '%s\n' "$tcpdump_errors" | sed -n '1,6p' | sed 's/^/ /'
|
|
fi
|
|
|
|
packet_total="$(wc -l <"$TCPDUMP_LOG" 2>/dev/null | tr -d ' ')"
|
|
is_number "$packet_total" || packet_total=0
|
|
|
|
related="$(tcpdump_related_lines)"
|
|
related_count="$(printf '%s\n' "$related" | awk 'NF { count++ } END { print count + 0 }')"
|
|
other_count=$((packet_total - related_count))
|
|
[ "$other_count" -ge 0 ] 2>/dev/null || other_count=0
|
|
|
|
printf 'captured: total=%s related=%s other_same_port=%s\n' \
|
|
"$packet_total" "$related_count" "$other_count"
|
|
|
|
if [ "$related_count" -gt 0 ]; then
|
|
printf '%s\n' "$related" | print_tcpdump_line_summary
|
|
else
|
|
printf 'related_packets: none\n'
|
|
fi
|
|
|
|
if [ "$other_count" -gt 0 ]; then
|
|
printf 'other_same_port_packets: %s hidden; use -v to inspect possible proxy outbound/noise\n' "$other_count"
|
|
fi
|
|
|
|
if [ "$VERBOSE" -eq 1 ] && [ "$packet_total" -gt 0 ]; then
|
|
printf 'raw_packet_tail:\n'
|
|
tail -n 80 "$TCPDUMP_LOG" | sed 's/^/ /'
|
|
fi
|
|
}
|
|
|
|
log_seed_lines() {
|
|
log_file="$1"
|
|
[ -s "$log_file" ] || return 1
|
|
|
|
remote_ip="$(remote_ip_from_curl)"
|
|
{
|
|
[ -n "$HOST" ] && grep -F "$HOST" "$log_file" 2>/dev/null
|
|
for target_ip in $TARGET_IPS $remote_ip; do
|
|
[ -n "$target_ip" ] && grep -F "$target_ip" "$log_file" 2>/dev/null
|
|
done
|
|
} | limit_unique_lines 80
|
|
}
|
|
|
|
extract_connection_ids() {
|
|
awk '
|
|
{
|
|
line = $0
|
|
while (match(line, /\[[0-9][0-9]*\]/)) {
|
|
id = substr(line, RSTART + 1, RLENGTH - 2)
|
|
if (!seen[id]++) print id
|
|
line = substr(line, RSTART + RLENGTH)
|
|
}
|
|
}
|
|
'
|
|
}
|
|
|
|
log_related_lines() {
|
|
log_file="$1"
|
|
[ -s "$log_file" ] || return 1
|
|
|
|
seeds="$(log_seed_lines "$log_file")"
|
|
[ -n "$seeds" ] || return 1
|
|
|
|
ids="$(printf '%s\n' "$seeds" | extract_connection_ids)"
|
|
{
|
|
printf '%s\n' "$seeds"
|
|
for id in $ids; do
|
|
grep -F "[$id]" "$log_file" 2>/dev/null
|
|
done
|
|
} | limit_unique_lines 100
|
|
}
|
|
|
|
print_log_summary() {
|
|
label="$1"
|
|
log_file="$2"
|
|
|
|
related="$(log_related_lines "$log_file")"
|
|
[ -n "$related" ] || return 1
|
|
|
|
printf '%s:\n' "$label"
|
|
|
|
issues="$(printf '%s\n' "$related" | grep -Ei 'error|failed|timeout|warn' 2>/dev/null | limit_unique_lines 8)"
|
|
dns="$(printf '%s\n' "$related" |
|
|
grep -Ei 'dns|lookup|resolve|resolved|query|answer|reply|cache' 2>/dev/null |
|
|
grep -Eiv 'error|failed|timeout|warn' 2>/dev/null |
|
|
limit_unique_lines 12)"
|
|
flow="$(printf '%s\n' "$related" |
|
|
grep -Ei 'inbound|outbound|routing|route|detour|accepted|connection|tproxy|mark' 2>/dev/null |
|
|
grep -Eiv 'error|failed|timeout|warn|dns|lookup|resolve|resolved|query|answer|reply|cache' 2>/dev/null |
|
|
limit_unique_lines 12)"
|
|
|
|
if [ -n "$issues" ]; then
|
|
printf ' issues:\n'
|
|
printf '%s\n' "$issues" | sed 's/^/ /'
|
|
fi
|
|
|
|
if [ -n "$dns" ]; then
|
|
printf ' dns:\n'
|
|
printf '%s\n' "$dns" | sed 's/^/ /'
|
|
fi
|
|
|
|
if [ -n "$flow" ]; then
|
|
printf ' flow:\n'
|
|
printf '%s\n' "$flow" | sed 's/^/ /'
|
|
fi
|
|
|
|
if [ -z "$issues" ] && [ -z "$dns" ] && [ -z "$flow" ]; then
|
|
printf ' matched:\n'
|
|
printf '%s\n' "$related" | limit_unique_lines 8 | sed 's/^/ /'
|
|
fi
|
|
|
|
if [ "$VERBOSE" -eq 1 ]; then
|
|
printf ' raw_related:\n'
|
|
printf '%s\n' "$related" | sed 's/^/ /'
|
|
fi
|
|
}
|
|
|
|
print_core_logs() {
|
|
print_section "Core logs"
|
|
|
|
collect_new_service_logs
|
|
|
|
printed=0
|
|
if print_log_summary "service" "$SERVICE_LOG"; then
|
|
printed=1
|
|
fi
|
|
|
|
if print_log_summary "system" "$SYS_LOG"; then
|
|
printed=1
|
|
fi
|
|
|
|
if [ "$printed" -eq 0 ]; then
|
|
printf 'no related core/system log lines captured\n'
|
|
printf 'hint: enable proxy/shunt logs or raise core log level for more detail\n'
|
|
fi
|
|
}
|
|
|
|
snapshot_log_positions
|
|
start_monitors
|
|
sleep 1
|
|
|
|
TARGET_IPS="$(resolve_host)"
|
|
|
|
if [ "$MODE" = "client" ]; then
|
|
add_client_trace_rules || die "failed to insert nft trace rule; see $NFT_SETUP_LOG"
|
|
else
|
|
add_router_trace_rules || die "failed to insert nft trace rule; see $NFT_SETUP_LOG"
|
|
fi
|
|
|
|
if [ "$ENABLE_TCPDUMP" -eq 1 ]; then
|
|
start_tcpdump || die "failed to start tcpdump on $TCPDUMP_IFACE; see $TCPDUMP_ERR"
|
|
fi
|
|
|
|
if [ "$MODE" = "client" ]; then
|
|
wait_for_client_request
|
|
else
|
|
run_router_curl
|
|
fi
|
|
|
|
sleep 1
|
|
stop_tcpdump
|
|
stop_monitors
|
|
|
|
print_request_summary
|
|
print_nft_summary
|
|
print_tcpdump_summary
|
|
print_core_logs
|