diff --git a/.gitignore b/.gitignore index 4a71c548..49132e68 100644 --- a/.gitignore +++ b/.gitignore @@ -137,6 +137,7 @@ lib/dialects/linux/tests/pidfd lib/dialects/linux/tests/pipe lib/dialects/linux/tests/pty lib/dialects/linux/tests/ux +lib/dialects/linux/tests/udp_state lib/dialects/linux/tests/mmap lib/dialects/linux/tests/unbal_parens diff --git a/Makefile.am b/Makefile.am index 75086ca3..e090353c 100644 --- a/Makefile.am +++ b/Makefile.am @@ -156,6 +156,7 @@ LINUX_TESTS = lib/dialects/linux/tests/case-10-mqueue.bash \ lib/dialects/linux/tests/case-20-inet-socket-endpoint.bash \ lib/dialects/linux/tests/case-20-mmap.bash \ lib/dialects/linux/tests/case-20-udp-socket-state.bash \ + lib/dialects/linux/tests/case-20-udp-state-filter.bash \ lib/dialects/linux/tests/case-20-mqueue-endpoint.bash \ lib/dialects/linux/tests/case-20-open-flags-cx.bash \ lib/dialects/linux/tests/case-20-open-flags-path.bash \ @@ -179,6 +180,7 @@ check_PROGRAMS += lib/dialects/linux/tests/epoll \ lib/dialects/linux/tests/pipe \ lib/dialects/linux/tests/pty \ lib/dialects/linux/tests/ux \ + lib/dialects/linux/tests/udp_state \ lib/dialects/linux/tests/unbal_parens lib_dialects_linux_tests_mq_fork_LDADD = -lrt lib_dialects_linux_tests_mq_open_LDADD = -lrt diff --git a/lib/dialects/linux/dsock.c b/lib/dialects/linux/dsock.c index 0b9dbbbc..34a95dfd 100644 --- a/lib/dialects/linux/dsock.c +++ b/lib/dialects/linux/dsock.c @@ -382,6 +382,51 @@ static void get_tcpudp6(struct lsof_context *ctx, char *p, int pr, int clr); static int hex_ipv6_to_in6(char *as, struct in6_addr *ad); #endif /* defined(HASIPv6) */ +/* + * excluded_by_state() -- check a TCP or UDP /proc record against the -s + * state inclusions and exclusions of its protocol + * return: 1 == exclude the file + * 0 == keep processing it + */ +static int excluded_by_state(struct lsof_context *ctx, /* context */ + int proto, /* 0 = TCP, else UDP or UDPLITE */ + int state) /* state from the /proc record */ +{ + unsigned char *inc, *exc; + int i, inn, excn, n, other_inn; + + if (proto == 0) { + i = state + TcpStOff; + n = TcpNstates; + inc = TcpStI; + exc = TcpStX; + inn = TcpStIn; + excn = TcpStXn; + other_inn = UdpStIn; + } else { + i = state + UdpStOff; + n = UdpNstates; + inc = UdpStI; + exc = UdpStX; + inn = UdpStIn; + excn = UdpStXn; + other_inn = TcpStIn; + } + if (excn && i >= 0 && i < n && exc[i]) + return (1); + if (inn) { + if (i >= 0 && i < n && inc[i]) { + inc[i] = 2; + return (0); + } + return (1); + } + /* + * Only states of the other protocol were asked for. + */ + return (other_inn ? 1 : 0); +} + /* * build_IPstates() -- build the TCP and UDP state tables */ @@ -3853,32 +3898,9 @@ void process_proc_sock(struct lsof_context *ctx, /* context */ * and foreign IPv6 addresses; save the type and protocol; and * (optionally) save the queue sizes. */ - i = tp6->state + TcpStOff; - if (TcpStXn) { - - /* - * Check for state exclusion. - */ - if (i >= 0 && i < TcpNstates) { - if (TcpStX[i]) { - Lf->sf |= SELEXCLF; - return; - } - } - } - if (TcpStIn) { - - /* - * Check for state inclusion. - */ - if (i >= 0 && i < TcpNstates) { - if (TcpStI[i]) - TcpStI[i] = 2; - else { - Lf->sf |= SELEXCLF; - return; - } - } + if (excluded_by_state(ctx, tp6->proto, tp6->state)) { + Lf->sf |= SELEXCLF; + return; } if (Fnet && (FnetTy != 4)) Lf->sf |= SELNET; @@ -3961,32 +3983,9 @@ void process_proc_sock(struct lsof_context *ctx, /* context */ * save the local and foreign IPv4 addresses; save the type and * protocol; and (optionally) save the queue sizes. */ - i = tp->state + TcpStOff; - if (TcpStXn) { - - /* - * Check for state exclusion. - */ - if (i >= 0 && i < TcpNstates) { - if (TcpStX[i]) { - Lf->sf |= SELEXCLF; - return; - } - } - } - if (TcpStIn) { - - /* - * Check for state inclusion. - */ - if (i >= 0 && i < TcpNstates) { - if (TcpStI[i]) - TcpStI[i] = 2; - else { - Lf->sf |= SELEXCLF; - return; - } - } + if (excluded_by_state(ctx, tp->proto, tp->state)) { + Lf->sf |= SELEXCLF; + return; } if (Fnet && (FnetTy != 6)) Lf->sf |= SELNET; diff --git a/lib/dialects/linux/tests/Makefile b/lib/dialects/linux/tests/Makefile index 06146158..71a8e185 100644 --- a/lib/dialects/linux/tests/Makefile +++ b/lib/dialects/linux/tests/Makefile @@ -7,6 +7,7 @@ HELPERS = \ pipe \ pty \ ux \ + udp_state \ mmap \ unbal_parens \ \ diff --git a/lib/dialects/linux/tests/case-20-udp-socket-state.bash b/lib/dialects/linux/tests/case-20-udp-socket-state.bash index 7185be52..f8ff74ed 100755 --- a/lib/dialects/linux/tests/case-20-udp-socket-state.bash +++ b/lib/dialects/linux/tests/case-20-udp-socket-state.bash @@ -38,6 +38,24 @@ if ! cat $fclient | grep -q "UDP.*127.0.0.1:.*->127.0.0.1:10001 (ESTABLISHED)"; exit 1 fi +$lsof -n -P -p $client -a -i UDP -s UDP:ESTABLISHED > $fclient +if ! cat $fclient | grep -q "UDP.*127.0.0.1:.*->127.0.0.1:10001 (ESTABLISHED)"; then + echo "connected UDP socket missing with -s UDP:ESTABLISHED" >> $report + cat $fclient >> $report + killBoth $client $server + rm -f $fclient + exit 1 +fi + +$lsof -n -P -p $client -a -i UDP -s UDP:^ESTABLISHED > $fclient +if cat $fclient | grep -q "UDP"; then + echo "connected UDP socket listed with -s UDP:^ESTABLISHED" >> $report + cat $fclient >> $report + killBoth $client $server + rm -f $fclient + exit 1 +fi + rm -f $fclient killBoth $client $server diff --git a/lib/dialects/linux/tests/case-20-udp-state-filter.bash b/lib/dialects/linux/tests/case-20-udp-state-filter.bash new file mode 100755 index 00000000..30324751 --- /dev/null +++ b/lib/dialects/linux/tests/case-20-udp-state-filter.bash @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +source tests/common.bash + +work=$(mktemp -d "${TMPDIR:-/tmp}/lsof-udp-state.XXXXXX") || exit 1 +pid= +failed=0 +cleanup() { + if [[ -n $pid ]]; then + kill "$pid" 2>/dev/null + wait "$pid" 2>/dev/null + fi + rm -rf "$work" +} +trap cleanup EXIT + +check_fds() { + local label=$1 expected=$2 status actual + shift 2 + "$lsof" -n -P -a -p "$pid" -Ff "$@" > "$work/output" 2> "$work/error" + status=$? + expected=$(printf '%s\n' "$expected" | tr ' ' '\n' | sort -n) + actual=$(sed -n 's/^f\([0-9][0-9]*\)$/\1/p' "$work/output" | sort -n) + if [[ $status -ne 0 || $actual != "$expected" ]]; then + echo "IPv$family $label: exit $status; expected FDs [$expected], got [$actual]" + cat "$work/output" "$work/error" + return 1 + fi +} + +{ + for family in 4 6; do + : > "$work/ready" + "$tcasedir/udp_state" "$family" > "$work/ready" 2> "$work/setup-error" & + pid=$! + for ((attempt = 0; attempt < 100; attempt++)); do + [[ -s $work/ready ]] && break + kill -0 "$pid" 2>/dev/null || break + sleep 0.05 + done + if [[ ! -s $work/ready ]]; then + if kill -0 "$pid" 2>/dev/null; then + echo "IPv$family socket setup timed out" + exit 1 + fi + wait "$pid" + status=$? + pid= + cat "$work/setup-error" + if [[ $family == 6 && $status == 77 ]]; then + echo "SKIP: IPv6 unavailable (IPv4 coverage passed)" + continue + fi + exit 1 + fi + read -r idle connected listener client server < "$work/ready" + + check_fds baseline "$idle $connected" -i "${family}UDP" || failed=1 + check_fds inclusion "$connected" -i "${family}UDP" -sUDP:ESTABLISHED || failed=1 + check_fds exclusion "$idle" -i "${family}UDP" '-sUDP:^ESTABLISHED' || failed=1 + check_fds mixed-inclusion "$listener $connected" -i "$family" \ + -sTCP:LISTEN -sUDP:ESTABLISHED || failed=1 + check_fds tcp-only "$listener" -i "$family" -sTCP:LISTEN || failed=1 + check_fds udp-only "$connected" -i "$family" -sUDP:ESTABLISHED || failed=1 + check_fds tcp-exclusion-only "$idle $connected $listener" -i "$family" \ + '-sTCP:^ESTABLISHED' || failed=1 + check_fds udp-exclusion-only "$idle $listener $client $server" -i "$family" \ + '-sUDP:^ESTABLISHED' || failed=1 + check_fds mixed-exclusion "$idle $listener" -i "$family" \ + '-sTCP:^ESTABLISHED' '-sUDP:^ESTABLISHED' || failed=1 + + kill "$pid" + wait "$pid" 2>/dev/null + pid= + echo "IPv$family UDP state filter checks completed" + done + exit "$failed" +} > "$report" 2>&1 diff --git a/lib/dialects/linux/tests/udp_state.c b/lib/dialects/linux/tests/udp_state.c new file mode 100644 index 00000000..572b20ec --- /dev/null +++ b/lib/dialects/linux/tests/udp_state.c @@ -0,0 +1,77 @@ +/* Keep connected/unconnected UDP and listening/connected TCP sockets in one PID. + * Print their descriptors only once all sockets are ready for inspection. + */ +#include +#include +#include +#include +#include +#include +#include + +static int bound_socket(int family, int type, struct sockaddr_storage *address, + socklen_t *length) { + int fd = socket(family, type, 0); + if (fd < 0) + return -1; + memset(address, 0, sizeof(*address)); + address->ss_family = family; + if (family == AF_INET) { + struct sockaddr_in *v4 = (struct sockaddr_in *)address; + v4->sin_addr.s_addr = htonl(INADDR_LOOPBACK); + *length = sizeof(*v4); + } else { + struct sockaddr_in6 *v6 = (struct sockaddr_in6 *)address; + v6->sin6_addr = in6addr_loopback; + *length = sizeof(*v6); + } + if (bind(fd, (struct sockaddr *)address, *length) < 0 || + getsockname(fd, (struct sockaddr *)address, length) < 0) { + int saved_errno = errno; + close(fd); + errno = saved_errno; + return -1; + } + return fd; +} + +int main(int argc, char **argv) { + struct sockaddr_storage udp_address, tcp_address; + socklen_t udp_length, tcp_length; + int family, udp_idle, udp_connected, tcp_listener, tcp_client, tcp_server; + + if (argc != 2 || (strcmp(argv[1], "4") && strcmp(argv[1], "6"))) + return 1; + family = !strcmp(argv[1], "4") ? AF_INET : AF_INET6; + udp_idle = bound_socket(family, SOCK_DGRAM, &udp_address, &udp_length); + if (udp_idle < 0) { + int unavailable = family == AF_INET6 && + (errno == EAFNOSUPPORT || errno == EADDRNOTAVAIL || + errno == EPROTONOSUPPORT); + perror("UDP bind"); + return unavailable ? 77 : 1; + } + udp_connected = socket(family, SOCK_DGRAM, 0); + if (udp_connected < 0 || + connect(udp_connected, (struct sockaddr *)&udp_address, udp_length) < 0) + goto error; + tcp_listener = bound_socket(family, SOCK_STREAM, &tcp_address, &tcp_length); + if (tcp_listener < 0 || listen(tcp_listener, 1) < 0) + goto error; + tcp_client = socket(family, SOCK_STREAM, 0); + if (tcp_client < 0 || + connect(tcp_client, (struct sockaddr *)&tcp_address, tcp_length) < 0) + goto error; + tcp_server = accept(tcp_listener, NULL, NULL); + if (tcp_server < 0) + goto error; + + printf("%d %d %d %d %d\n", udp_idle, udp_connected, tcp_listener, + tcp_client, tcp_server); + fflush(stdout); + for (;;) + pause(); +error: + perror("socket setup"); + return 1; +}