From c8860532ddac762c0cc9f9cb2ddbf9c479f136cb Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Sun, 12 Jul 2026 18:47:32 +0100 Subject: [PATCH] tests: fix the FTP check for unexpected RST - In vsftpd ignore unrelated RST by matching tcpdump RSTs to the data connection port pair. Prior to this change an unrelated RST on a recycled ephemeral port could cause test failure. Closes https://github.com/curl/curl/pull/22305 --- tests/http/test_30_vsftpd.py | 4 ++-- tests/http/test_31_vsftpds.py | 4 ++-- tests/http/test_32_ftps_vsftpd.py | 4 ++-- tests/http/testenv/curl.py | 19 ++++++++++++++++--- tests/http/testenv/vsftpd.py | 9 +++++---- 5 files changed, 27 insertions(+), 13 deletions(-) diff --git a/tests/http/test_30_vsftpd.py b/tests/http/test_30_vsftpd.py index d625852249..6627be2613 100644 --- a/tests/http/test_30_vsftpd.py +++ b/tests/http/test_30_vsftpd.py @@ -161,7 +161,7 @@ class TestVsFTPD: # look only at ports from DATA connection. data_ports = vsftpd.get_data_ports(r) assert len(data_ports), f'unable to find FTP data port connected to\n{r.dump_logs()}' - assert len(r.tcpdump.get_rsts(ports=data_ports)) == 0, 'Unexpected TCP RST packets' + assert len(r.tcpdump.get_rsts(port_pairs=data_ports)) == 0, 'Unexpected TCP RST packets' # check with `tcpdump` if curl causes any TCP RST packets @pytest.mark.skipif(condition=not Env.tcpdump(), reason="tcpdump not available") @@ -182,7 +182,7 @@ class TestVsFTPD: # look only at ports from DATA connection. data_ports = vsftpd.get_data_ports(r) assert len(data_ports), f'unable to find FTP data port connected to\n{r.dump_logs()}' - assert len(r.tcpdump.get_rsts(ports=data_ports)) == 0, 'Unexpected TCP RST packets' + assert len(r.tcpdump.get_rsts(port_pairs=data_ports)) == 0, 'Unexpected TCP RST packets' def test_30_08_active_download(self, env: Env, vsftpd: VsFTPD): docname = 'data-10k' diff --git a/tests/http/test_31_vsftpds.py b/tests/http/test_31_vsftpds.py index 18f93dede4..1d8a897fbd 100644 --- a/tests/http/test_31_vsftpds.py +++ b/tests/http/test_31_vsftpds.py @@ -177,7 +177,7 @@ class TestVsFTPD: # look only at ports from DATA connection. data_ports = vsftpds.get_data_ports(r) assert len(data_ports), f'unable to find FTP data port connected to\n{r.dump_logs()}' - assert len(r.tcpdump.get_rsts(ports=data_ports)) == 0, 'Unexpected TCP RST packets' + assert len(r.tcpdump.get_rsts(port_pairs=data_ports)) == 0, 'Unexpected TCP RST packets' # check with `tcpdump` if curl causes any TCP RST packets @pytest.mark.skipif(condition=not Env.tcpdump(), reason="tcpdump not available") @@ -197,7 +197,7 @@ class TestVsFTPD: # look only at ports from DATA connection. data_ports = vsftpds.get_data_ports(r) assert len(data_ports), f'unable to find FTP data port connected to\n{r.dump_logs()}' - assert len(r.tcpdump.get_rsts(ports=data_ports)) == 0, 'Unexpected TCP RST packets' + assert len(r.tcpdump.get_rsts(port_pairs=data_ports)) == 0, 'Unexpected TCP RST packets' def test_31_08_upload_ascii(self, env: Env, vsftpds: VsFTPD): docname = 'upload-ascii' diff --git a/tests/http/test_32_ftps_vsftpd.py b/tests/http/test_32_ftps_vsftpd.py index df1258c2d2..1e2e965292 100644 --- a/tests/http/test_32_ftps_vsftpd.py +++ b/tests/http/test_32_ftps_vsftpd.py @@ -187,7 +187,7 @@ class TestFtpsVsFTPD: # look only at ports from DATA connection. data_ports = vsftpds.get_data_ports(r) assert len(data_ports), f'unable to find FTP data port connected to\n{r.dump_logs()}' - assert len(r.tcpdump.get_rsts(ports=data_ports)) == 0, 'Unexpected TCP RST packets' + assert len(r.tcpdump.get_rsts(port_pairs=data_ports)) == 0, 'Unexpected TCP RST packets' # check with `tcpdump` if curl causes any TCP RST packets @pytest.mark.skipif(condition=not Env.tcpdump(), reason="tcpdump not available") @@ -207,7 +207,7 @@ class TestFtpsVsFTPD: # look only at ports from DATA connection. data_ports = vsftpds.get_data_ports(r) assert len(data_ports), f'unable to find FTP data port connected to\n{r.dump_logs()}' - assert len(r.tcpdump.get_rsts(ports=data_ports)) == 0, 'Unexpected TCP RST packets' + assert len(r.tcpdump.get_rsts(port_pairs=data_ports)) == 0, 'Unexpected TCP RST packets' def test_32_08_upload_ascii(self, env: Env, vsftpds: VsFTPD): docname = 'upload-ascii' diff --git a/tests/http/testenv/curl.py b/tests/http/testenv/curl.py index 90b7b28856..2abbbbe49c 100644 --- a/tests/http/testenv/curl.py +++ b/tests/http/testenv/curl.py @@ -36,7 +36,7 @@ from datetime import datetime, timedelta, timezone from functools import cmp_to_key from statistics import fmean, mean from threading import Thread -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Tuple, Union from urllib.parse import urlparse import psutil @@ -188,9 +188,19 @@ class RunTcpDump: self._stdoutfile = os.path.join(self._run_dir, 'tcpdump.out') self._stderrfile = os.path.join(self._run_dir, 'tcpdump.err') - def get_rsts(self, ports: Optional[List[int]] = None) -> Optional[List[str]]: + def get_rsts(self, ports: Optional[List[int]] = None, + port_pairs: Optional[List[Tuple[int, int]]] = None + ) -> Optional[List[str]]: if self._proc: raise Exception('tcpdump still running') + # a pair matches only a RST between exactly these two ports, while + # a port in `ports` matches any RST it is involved in + pairs = None + if port_pairs is not None: + pairs = set() + for p1, p2 in port_pairs: + pairs.add((p1, p2)) + pairs.add((p2, p1)) lines = [] with open(self._stdoutfile) as fd: for line in fd: @@ -198,7 +208,10 @@ class RunTcpDump: if m: sport = int(m.group(1)) dport = int(m.group(2)) - if ports is None or sport in ports or dport in ports: + if pairs is not None: + if (sport, dport) in pairs: + lines.append(line) + elif ports is None or sport in ports or dport in ports: lines.append(line) return lines diff --git a/tests/http/testenv/vsftpd.py b/tests/http/testenv/vsftpd.py index daaf904f1e..e2c33edee3 100644 --- a/tests/http/testenv/vsftpd.py +++ b/tests/http/testenv/vsftpd.py @@ -31,7 +31,7 @@ import socket import subprocess import time from datetime import datetime, timedelta -from typing import Dict, List +from typing import Dict, List, Tuple from .curl import CurlClient, ExecResult from .env import Env @@ -226,6 +226,7 @@ class VsFTPD: with open(self._conf_file, 'w') as fd: fd.write("\n".join(conf)) - def get_data_ports(self, r: ExecResult) -> List[int]: - return [int(m.group(1)) for line in r.trace_lines if - (m := re.match(r'.*Established 2nd connection to .* \(\S+ port (\d+)\)', line))] + def get_data_ports(self, r: ExecResult) -> List[Tuple[int, int]]: + return [(int(m.group(1)), int(m.group(2))) for line in r.trace_lines if + (m := re.match(r'.*Established 2nd connection to .* ' + r'\(\S+ port (\d+)\) from \S+ port (\d+)', line))]