← All TILs · linux

Checking new firewall rules with nc and Python: open, refused, or dropped

linux - 2026-09-29

The security team has opened these flows for dar-servera:

Direction Flow Ports
In from dar-serverb and laptop-abdel 80 (http), 443 (https), 22 (ssh), 3306 (mysql)
Out to the Git server 22 (ssh), 443 (https)

Checking them takes two tools that are almost always there, nc and python3. It also takes knowing which of three answers you got. Everything below was run with OpenBSD nc 1.234 (the Debian/Ubuntu netcat-openbsd), Nmap ncat 7.99 (what nc is on RHEL), and Python 3.11.

The three answers a TCP check can give

nc -zv -w 3 dar-servera 3306

-z connects and closes without sending data, -v prints the result, and -w 3 gives up after 3 seconds.

Result OpenBSD nc ncat Meaning
Open Connection to … 3306 port [tcp/mysql] succeeded! Ncat: Connected to …:3306. Flow allowed, service listening
Refused connect to … port 3306 (tcp) failed: Connection refused Ncat: Connection refused. The firewall let it through; nothing listens on that port
Dropped connect to … port 3306 (tcp) timed out Ncat: TIMEOUT. No answer: a firewall is dropping it (or the host is down)

"Refused" is good news for the firewall ticket: the packet reached dar-servera, and its kernel answered. "Timed out" is the one to send back to the security team.

Two variants to recognize: - No route to host usually means a firewall that rejects instead of dropping. firewalld's default reject does that. - Name or service not known is DNS, not the firewall.

Both commands exit with 0 for open and 1 otherwise, so they work in a loop:

for p in 80 443 22 3306; do nc -zv -w 3 dar-servera "$p"; done

Before the service exists: listen yourself

Rules often open before MySQL is installed, so every check would say "refused". Put a listener on dar-servera first, then test from dar-serverb or laptop-abdel:

# on dar-servera; ports below 1024 need sudo, and the port must be free
sudo nc -lk 3306      # OpenBSD nc; ncat: sudo ncat -lk 3306
# on dar-serverb or laptop-abdel
nc -zv -w 3 dar-servera 3306

Without -k, the listener exits after the first connection. For 80, Python does the same job and also answers HTTP: sudo python3 -m http.server 80. Stop the listener before the real service starts; it holds the port.

Outbound from dar-servera to Git

Run from dar-servera, replacing github.com with your Git server:

nc -zv -w 3 github.com 22
nc -zv -w 3 github.com 443

Then use the real protocol. It also checks what a port test can't, such as a TLS-inspecting proxy or the SSH host key:

ssh -T git@github.com                      # "Permission denied (publickey)" still proves port 22 works
curl -sS -o /dev/null -w '%{http_code}\n' https://github.com
git ls-remote https://github.com/abdelhousni/til HEAD

UDP: nc -uz can't tell you

UDP has no handshake, so there's no "connected" to report. Probing an address that doesn't answer:

$ nc -uzv -w 2 10.255.255.1 5140
Connection to 10.255.255.1 5140 port [udp/*] succeeded!

ncat -uz says UDP packet sent successfully. Both exit 0 for a packet that went nowhere. The only way to prove a UDP flow is to have something on the other side print what arrives:

nc -ul 5140                                     # on the receiving host
echo "hello from laptop-abdel" | nc -u -w 1 dar-servera 5140   # on the sender

If the text appears on the receiving host, the flow works. If nothing appears, it doesn't, whatever the sender printed.

Without nc: Python

portcheck.py, standard library only:

#!/usr/bin/env python3
"""portcheck.py HOST PORT [PORT...] -- TCP: open, refused or dropped."""
import socket, sys

host, ports = sys.argv[1], sys.argv[2:]
for port in ports:
    try:
        with socket.create_connection((host, int(port)), timeout=3):
            result = "open"
    except ConnectionRefusedError:
        result = "refused  (host reached, nothing listening)"
    except (socket.timeout, TimeoutError):
        result = "dropped  (no answer: firewall?)"
    except OSError as e:
        result = f"error    ({e.strerror or e})"
    print(f"{host}:{port:<5} {result}")

Example output, while MySQL isn't installed yet:

$ python3 portcheck.py dar-servera 80 443 22 3306
dar-servera:80    open
dar-servera:443   open
dar-servera:22    open
dar-servera:3306  refused  (host reached, nothing listening)

A UDP listener, and a sender, in one line each:

python3 -c 'import socket;s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM);s.bind(("0.0.0.0",5140));print(*s.recvfrom(1024))'
python3 -c 'import socket;socket.socket(socket.AF_INET,socket.SOCK_DGRAM).sendto(b"ping",("dar-servera",5140))'

One more thing to rule out

A timeout can also come from the host firewall on dar-servera itself, not the network one: firewalld, ufw, or nftables. Check with sudo firewall-cmd --list-all or sudo nft list ruleset before reopening the ticket.

Sources

Created 2026-09-29T13:18:31+02:00 · Edit