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
This commit is contained in:
Graham Campbell 2026-07-12 18:47:32 +01:00 committed by Jay Satiro
parent 545cdd4b50
commit c8860532dd
5 changed files with 27 additions and 13 deletions

View file

@ -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'

View file

@ -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'

View file

@ -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'

View file

@ -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

View file

@ -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))]