Home › Topics › Firewalls & NAT › Troubleshooting

A Firewall Troubleshooting Playbook for Transfers

"Connection timed out." That is the entire error message, and the job has been producing it since three in the morning. A transfer that fails because of a firewall almost never says so. It says "connection timed out," or "failed to retrieve directory listing," or nothing at all. The job does not finish, and the only evidence is its absence. The firewall may have logged a single line among thousands, or nothing, because dropping packets silently is what it is for. Between those two silences, a junior administrator can lose a day guessing. This playbook replaces the guessing with a fixed order of steps.

You will learn to read the three failure signatures TCP gives you before any tool is opened. You will learn to reproduce a failure from both ends and from a third vantage point. You will learn to find the one firewall log line that matters and take a packet-capture spot check that settles arguments. You will also learn to match the classic symptoms — listing hangs, timeouts after login, works inside but not outside — to causes and fixes. Each step shows the command, the output, and what the output means. The order matters more than the tools; I have watched the right tools used in the wrong order for an entire afternoon.

This is the fifth article in our Firewalls, NAT, and File Transfer series. It is the firewall-specific chapter of the general method in our troubleshooting failed transfers series. Use that series to decide that the problem is a firewall problem; use this one to find which firewall, which rule, and which fix.

Step 0: Learn the Three Signatures

Every failed TCP connection fails in one of three ways, and each points somewhere different. Learn to tell them apart before you touch anything.

  • Refused — the client sent a SYN and got back a RST (reset) packet at once. Something at the destination answered "nothing is listening here." It is not a silent drop. It usually means the service is not running, listens on a different port, or a host firewall is set to reject rather than drop. It rarely means a network firewall; those drop silently.
  • Timed out — the client sent a SYN, retried it several times, and heard nothing. The packet was dropped somewhere along the path, or the reply was dropped on the way back. This is the classic firewall signature, and also the signature of a missing NAT forward or a wrong announced address.
  • Reset after connecting — the connection was established, worked for a while, then ended abruptly with a RST or "connection closed by peer." Something decided to end the session: an inspection device that disliked the content, an idle timeout, or an application that gave up. Firewall-related when it happens at a predictable point, such as the first encrypted command of an FTPS session.

The nc tool shows all three plainly:

$ nc -vz -w 5 198.51.100.20 22
Connection to 198.51.100.20 22 port [tcp/ssh] succeeded!

$ nc -vz -w 5 198.51.100.20 2222
nc: connect to 198.51.100.20 port 2222 (tcp) failed: Connection refused

$ nc -vz -w 5 198.51.100.20 50007
nc: connect to 198.51.100.20 port 50007 (tcp) failed: Operation timed out

Port 22 answered; port 2222 exists as a host but has nothing listening; port 50007 vanished into a firewall. Everything else in this playbook is about the third case.

Acme lost most of a day to the first signature before anyone read it. A new partner reported "connection refused" on port 22. Because the word firewall was in the ticket title, the morning went on tracing perimeter rules that turned out to be correct. Refused is rarely what a network firewall says; a network firewall says nothing. In the afternoon somebody ran ss -ltn on the server and found the SFTP service listening on port 2222. A hardening script had moved it there the week before without telling anyone. The rule was right, the partner was right, and the server had quietly moved house. The ticket template gained a first question that reads "refused, or timed out?"

Step 1: Gather the Facts

Five minutes of fact-gathering saves hours of testing. Before running anything, write down:

  1. The two ends. Client address as the client sees it, and as the server sees it (the public address after NAT). Server address as the server sees it, and as the client sees it. If those four are not all known, that is your first finding.
  2. Protocol, port, and mode. SFTP on 22? FTPS explicit on 21? Passive or active? The mode decides whether a passive range is involved at all.
  3. Where it fails. Before login (control connection), after login (data connection), or mid-transfer (session drop). Login-then-hang means data connection; nothing else matters until that is fixed.
  4. When it last worked and what changed. A firewall change, a new partner address, a server move, an OS patch that re-enabled a host firewall. Most firewall failures follow a change someone considered unrelated.
  5. Who else is affected. One partner or all? One direction or both? "Only partner X" points at an allowlist; "everyone since Tuesday" points at a rule.

These are the same facts the shared connection sheet in the partner article records permanently. If you keep one, step 1 takes thirty seconds. If you do not, step 1 is four emails and a phone call.

Step 2: Reproduce From Both Sides

A firewall problem has a location, and the way to find it is to test from more than one place. Three vantage points, in order.

From the client

Test each port the protocol needs, separately, with a plain TCP connection: PowerShell's Test-NetConnection on Windows, nc elsewhere. For FTP, that means port 21 and a port from the passive range:

PS C:\> Test-NetConnection ftp.example.com -Port 21 | Select RemoteAddress,TcpTestSucceeded
RemoteAddress  TcpTestSucceeded
-------------  ----------------
203.0.113.80               True

PS C:\> Test-NetConnection ftp.example.com -Port 50007 | Select RemoteAddress,TcpTestSucceeded
WARNING: TCP connect to (203.0.113.80 : 50007) failed
203.0.113.80              False

Then run the real client with verbose output and read the negotiation. With curl, -v prints every reply, including the passive reply and the address the client then tries:

$ curl -v --user alex ftp://ftp.example.com/ 2>&1 | grep -E '227|Trying|failed'
< 227 Entering Passive Mode (203,0,113,80,195,87)
*   Trying 203.0.113.80:50007...
* connect to 203.0.113.80 port 50007 failed: Connection timed out

The 227 line answers two questions at once: is the announced address right (it is, here), and which port did the server pick (50007)? If the address were 10.0.5.20, you would stop and go to the announced-address fix in NAT types. Graphical clients show the same lines in their message log; diagnosing FTP mode failures walks through reading them.

From the server

Confirm the service is listening on the ports the firewall is supposed to deliver:

C:\> netstat -ano | findstr LISTENING | findstr ":21 :22 :990"
  TCP    0.0.0.0:21      0.0.0.0:0      LISTENING     2288
  TCP    0.0.0.0:22      0.0.0.0:0      LISTENING     2288

$ ss -ltn '( sport = :21 or sport = :22 )'
State   Recv-Q  Send-Q   Local Address:Port   Peer Address:Port
LISTEN  0       128            0.0.0.0:21          0.0.0.0:*
LISTEN  0       128            0.0.0.0:22          0.0.0.0:*

Passive ports appear only during a transfer — the server opens each on demand — so watch during a live attempt. Run the listing from the client while ss -tn on the server shows whether a connection to port 50007 ever arrives. If the server never sees it, the drop is upstream. If it sees a SYN-RECV that never completes, the reply is being dropped on the way back. That points at asymmetric routing or a NAT rule that translates one direction only. The server's own activity log gives the same answer from the application's side. A server such as Sysax Multi Server logs each session and transfer, so a login with no data connection is visible there too.

From a third place

Test from a machine that is neither the client nor the server. Examples are a workstation outside the office, a colleague at another site, or a cloud shell. This separates "works inside but not outside" from "works outside but not inside," which have opposite causes. Inside-only means the DNAT forward or perimeter rule is missing. Outside-only means the hairpin is missing. Both are rows in the symptom table below. A colleague at another site is the cheapest network tool there is.

Step 3: Read the Firewall Logs

With a time and a source address from step 2, the firewall log becomes a lookup rather than a search. You are looking for one of three things.

A deny line for the connection. The best outcome: the firewall saw the packet and refused it, and the line says why. On a Linux firewall with the logging deny described in the rule design article:

$ sudo grep XFER-DENY /var/log/kern.log | grep 'SRC=203.0.113.5' | tail -2
Mar 14 02:10:07 fw1 kernel: XFER-DENY IN=eth0 OUT=eth1 SRC=203.0.113.5 DST=10.0.5.20 PROTO=TCP SPT=49813 DPT=50007 SYN
Mar 14 02:10:08 fw1 kernel: XFER-DENY IN=eth0 OUT=eth1 SRC=203.0.113.5 DST=10.0.5.20 PROTO=TCP SPT=49813 DPT=50007 SYN

Field by field: IN=eth0 is the outside interface, and SRC is the client's public address. DST is the server (already translated by DNAT, so the forward exists). DPT=50007 is the passive port, and SYN is a new connection. Conclusion: the forward works; the filter rule for the passive range does not cover 50007. On Windows Defender Firewall the same line lives in pfirewall.log once dropped-packet logging is enabled with netsh advfirewall set allprofiles logging droppedconnections enable. After date and time, the columns are action, protocol, source, destination, source port, destination port:

02:10:07 DROP TCP 203.0.113.5 10.0.5.20 49813 50007 52 S 3141592 0 64240 - - - RECEIVE

No line at all. If the firewall logs denies on transfer ports and there is no line for the attempt, the packet never reached this firewall. Look upstream: a second NAT device, the client's own firewall, the provider. This is where the third-vantage-point test pays off. A firewall cannot deny what it never saw.

An accept line with no reply. Some firewalls log accepted new connections too. An accept followed by silence means the firewall passed the SYN and the server did not answer. That means the host firewall on the server, or the service not listening, or a reply routed around the firewall. Go back to the server-side checks.

Whatever the log says, note the rule it cites. Next-generation firewalls add the application and threat they identified. When that field says something unexpected for transfer traffic, you have found an inspection problem, covered in application helpers and ALGs. Reading transfer logs covers the server-side logs you read alongside.

Remember: a timeout is a dropped packet, and a dropped packet leaves exactly one of two traces. It leaves a deny line on the device that dropped it, or nothing on the devices that never saw it. Finding which device has the line, or which is the first with no line, is the entire troubleshooting problem.

Step 4: Packet-Capture Spot Checks

A packet capture is the evidence that ends debates between teams, and for firewall problems it needs only a handful of packets. The question is always the same: did the SYN arrive, and was it answered? Packets have no opinions, which is their main advantage in a meeting.

On the server (or on the firewall's inside interface), capture the client's traffic to the transfer ports and watch a listing attempt:

$ sudo tcpdump -ni eth0 'host 203.0.113.5 and (port 21 or portrange 50000-50100)'
02:10:05.201 IP 203.0.113.5.49812 > 10.0.5.20.21: Flags [S], seq 3141592, win 64240, length 0
02:10:05.201 IP 10.0.5.20.21 > 203.0.113.5.49812: Flags [S.], seq 2718281, ack 3141593, win 65160, length 0
02:10:05.225 IP 203.0.113.5.49812 > 10.0.5.20.21: Flags [.], ack 1, win 502, length 0
   ... login and PASV exchange ...
02:10:07.114 IP 203.0.113.5.49813 > 10.0.5.20.50007: Flags [S], seq 1618033, win 64240, length 0
02:10:08.130 IP 203.0.113.5.49813 > 10.0.5.20.50007: Flags [S], seq 1618033, win 64240, length 0
02:10:10.162 IP 203.0.113.5.49813 > 10.0.5.20.50007: Flags [S], seq 1618033, win 64240, length 0

The first three lines are a healthy three-way handshake on port 21: SYN ([S]), SYN-ACK ([S.]), ACK ([.]). The last three are a SYN to port 50007 repeated at one, two, and four seconds with no SYN-ACK. Here the SYN reached the server and the server did not answer. The network firewall is innocent; the host firewall or the service is the suspect. Had no SYN to 50007 appeared at all, the packet was dropped before the server, and the network firewall log is where to look. Two captures, one on each side of a firewall, bracket the drop exactly. At that point the conversation gets much shorter.

For readers of a graphical analyzer, these display filters are worth memorizing. In words, "SYN packets with no ACK flag" (tcp.flags.syn == 1 && tcp.flags.ack == 0) shows every connection attempt. "Retransmissions" (tcp.analysis.retransmission) highlights the ones that went unanswered. "Reset packets" (tcp.flags.reset == 1) finds who ended a session. "FTP passive replies" (ftp.response.code == 227) shows the announced address and port in one click. That is how you catch a helper rewriting it.

Keep captures short and specific — one host, one port range, one attempt. An hour of everything on the interface is where evidence goes to hide.

The Classic Symptom Table

Most firewall tickets fall into a dozen patterns. The table maps each to its most likely cause, the test that confirms it, and the fix.

Symptom Most likely cause Confirm with Fix
Login works, LIST hangs, then times out Passive range not allowed or not forwarded nc to a passive port times out; deny line with DPT in range Allow and forward the exact range
Login works, immediate 425 Can't open data connection Server cannot open a passive port (range exhausted) or data connection came from a different address (NAT pool) Server log at that timestamp Widen range; pin one egress address
Client tries to connect to a 10.x or 192.168.x address Server announces its private address The 227 line in verbose output Set the announced address on the server
Plain FTP fine, FTPS dies at first listing FTP helper cannot read the encrypted channel; no pinhole or session reset Works once a static passive-range rule is added Static rule; helper off for that rule
Works from outside, not from inside No hairpin NAT for the public address Inside client to the private address works Split DNS or NAT reflection
Works from inside, not from outside DNAT forward or perimeter allow missing; or an outer NAT (double NAT) Third-vantage test; no deny line means upstream Add forward and rule on every NAT device
Connection refused instantly Service not listening, wrong port, or host firewall set to reject ss -ltn / netstat on the server Start the service; fix the port
Works for partner A, not partner B Partner B's address missing from the allowlist, or B changed address Deny line with B's source address Update the partner address set
Most transfers work, some hang at random Server range wider than firewall range Failing sessions' 227 port is outside the rule Make the ranges identical
Large transfers die part-way; small ones succeed Idle control connection expired from a NAT or firewall state table Failure time matches the device's idle timeout Keepalives; see the timeouts series

A Worked Example

A partner reports: "Since Tuesday, our nightly pull logs in fine and then hangs on the directory listing." The server team says nothing changed. Both statements will turn out to be true. Apply the playbook.

Step 0. The partner's log shows a timeout, not a refusal. Silent drop. Step 1. Partner egress is 203.0.113.5; our server is 10.0.5.20 behind 203.0.113.80. The connection uses explicit FTPS in passive mode, range 50000–50100. It fails after login. "Tuesday" is the day the network team migrated to a new perimeter firewall. Step 2. From the partner, port 21 succeeds and port 50007 times out. From inside, both succeed. From a cloud shell, 21 succeeds, 50007 times out. Outside-only failure on the passive range: the perimeter, not the server. Step 3. The new firewall's log:

Mar 14 02:10:07 fw2 kernel: XFER-DENY IN=eth0 OUT=eth1 SRC=203.0.113.5 DST=10.0.5.20 PROTO=TCP SPT=49813 DPT=50037 SYN

Port 50037 denied. The migrated rule reads 50000-50010 — a transcription error. Ports 50000 to 50010 work, which is why the daytime tests by the network team passed. The nightly job, arriving when the server had a dozen sessions open, was handed a higher port. Step 4 was not needed, but a capture on the server would have shown no SYN to 50037 at all, confirming the drop was upstream. Fix: correct the rule to 50000-50100 and retest from the partner and the cloud shell. Then update the rule register with the ticket number and the reason. Total time, once the steps are followed in order: under an hour.

The Ten-Command Kit

Everything above, as a copyable sequence. Replace the addresses and ports with your own.

# From the client
Test-NetConnection ftp.example.com -Port 21            # control port reachable?
Test-NetConnection ftp.example.com -Port 50007         # a passive port reachable?
nc -vz -w 5 ftp.example.com 22                         # same on Linux/macOS
curl -v --user alex ftp://ftp.example.com/ 2>&1 | grep 227   # announced address and port
netstat -ano | findstr SYN_SENT                        # what is stuck, right now

# On the server
ss -ltn                                                # what is listening
ss -tn | grep 5000                                     # did a passive connection arrive?
sudo tcpdump -ni eth0 'host 203.0.113.5 and portrange 50000-50100'

# On the firewall
sudo grep XFER-DENY /var/log/kern.log | grep SRC=203.0.113.5 | tail
sudo conntrack -L | grep 203.0.113.5                   # what state does it hold?

The Short Version

Refused means something answered; timed out means something dropped; reset means something ended it. Gather the four addresses, the protocol and mode, the failure point, and what changed. Test each port separately from the client, confirm the server is listening, and test from a third place to split inside-only from outside-only. Find the deny line or the first device with no line. When teams disagree, capture on both sides of the firewall and look for the SYN with no SYN-ACK. Match the symptom to the table and apply the fix. Write the cause into the rule register so the next person starts from your answer instead of from zero. The firewall will never explain itself. The register can.

Most of the fixes lead back into the rest of this series. The announced address is in NAT types, and the helper in application helpers and ALGs. The rule and range are in rule design. The fix may belong on the partner's firewall rather than yours. In that case, the closing article, coordinating firewall changes with partners, is about getting it made without breaking anything else.

Frequently Asked Questions

What is the difference between "connection refused" and "connection timed out"?
Refused means a host at that address answered that nothing is listening on the port, so the packet arrived and was rejected. Timed out means no answer came at all, which is what a firewall dropping the packet looks like. Refused points at the service; timed out points at the path.
Why test a passive port with a plain TCP tool instead of the FTP client?
Because it isolates the firewall question from the protocol. If a bare connection to port 50007 times out, no FTP setting on either side will help until the rule is fixed. The FTP client's verbose log then tells you which port and address the server is actually announcing.
The firewall log shows nothing for the failed connection. What does that mean?
If the firewall logs denies on those ports and there is no line, the packet never reached it. Look upstream: the client's own firewall, a second NAT device, or the provider. Testing from a third location helps locate where the packet is lost.
How do I read a SYN with no SYN-ACK in a capture?
A SYN is the "may I connect?" packet; a SYN-ACK is the "yes" from the server. A SYN repeated at growing intervals with no SYN-ACK means the request was never answered. If the capture is on the server, the server or its host firewall dropped it. If the capture is before a firewall and the SYN never appears after it, the firewall did.
Why does the transfer work from the office but not from home?
Inside-only failure means the path from outside is missing something: a port forward, a perimeter rule, or a second NAT device in front of the firewall. Test from a third location to confirm, then check the perimeter log for a deny line with the outside address.

From the Sysax team: we build secure file transfer software for Windows. Sysax Multi Server is an FTP, FTPS, SFTP, and HTTPS server. Sysax FTP Automation handles scheduled, scripted transfers. Free trials are on the download page.