DEV Community

mattleeee
mattleeee

Posted on Originally published at hkcode.dpdns.org

Firewall Forensics: PAT Sessions, NAT44 Hit Counters and the Missing UDP Rule

A GB28181 camera was sending heartbeats. The SIP server saw them. The firewall session table confirmed the outbound translation. And yet inbound INVITEs never arrived — the NAT44 hit counter for UDP 7100 sat at exactly 0 while TCP 7100 on the same port showed 13 hits. That asymmetry is the whole story, and it's the kind of clue that gets buried under "the network is broken" tickets.

Here's how to read that evidence, why it matters, and how to turn a vague complaint into a reproducible pass/fail list.

The setup

GB28181 is a Chinese video surveillance standard built on SIP. Devices register to a platform, then receive INVITEs to start streaming. The signaling runs over UDP 7100 in this deployment. A camera at 10.0.13.200 sits behind a firewall doing PAT (Port Address Translation, the overloaded form of NAT44). The platform sits somewhere upstream, reachable only through a public IP.

The complaint: "the camera registers but we can't pull streams." Classic. Registration is outbound, streaming setup is inbound, and everything in between is where the bodies are buried.

Reading the session table

The first useful artifact is the firewall's session table. On most enterprise firewalls it looks something like this:

Protocol  Src              Sport  Dst              Dport  TransSrc         TransSport  State
UDP       10.0.13.200      7100   203.0.113.40     7100   198.51.100.77    20250       ACTIVE
TCP       10.0.13.200      7100   203.0.113.40     7100   198.51.100.77    20251       ESTABLISHED
Enter fullscreen mode Exit fullscreen mode

Two things to notice.

First, the outbound heartbeat from 10.0.13.200:7100 was translated to 198.51.100.77:20250. That's PAT doing its job. The source port changed from 7100 to 20250 — the public side sees a different port entirely. This is normal and expected; the camera has no idea its port was rewritten.

Second, the session is ACTIVE, which means the firewall has state for it. Heartbeats keep it alive. So the NAT rule matched, the translation happened, and the packet left the building.

Now the counter that actually matters:

NAT44 policy hit counters:
  UDP 7100: 0
  TCP 7100: 13
Enter fullscreen mode Exit fullscreen mode

Zero. On UDP. Thirteen on TCP.

This is the moment where a lot of troubleshooting goes wrong. People see "0 hits" and assume the NAT rule is broken. It isn't. The rule matched — we just watched it translate a packet. The counter is telling us something narrower and more useful: inbound packets on UDP 7100 never reached the NAT policy at all.

NAT and security policy are different layers

This trips up experienced engineers, so it's worth being explicit.

A firewall evaluates packets in order:

  1. Ingress interface / zone assignment
  2. Security policy (allow/deny between zones, possibly with application inspection)
  3. NAT policy (translation, including destination NAT for inbound)
  4. Session creation and forwarding

Inbound packets hit the security policy first, then NAT. If a packet is dropped by the security policy, or by an upstream filter that runs before the firewall even sees it, the NAT policy never increments its counter. The counter isn't a measure of "did the rule work" — it's a measure of "did any packet reach this rule."

So UDP 7100 hits = 0 means one of:

  • The packet never arrived at the firewall.
  • The packet arrived but was dropped by security policy, ASPF, or a DDoS/blacklist mechanism before NAT.
  • The packet arrived on a different interface or zone than expected.

In this case, the outbound session proves the NAT rule is correct and functional. The zero counter on inbound proves the problem is upstream of the NAT decision. The two most likely culprits:

  • The ISP modem has no UDP mapping. If the modem is doing its own NAT (double NAT), it needs a port-forward for UDP 7100 to reach the firewall. Many consumer and SMB modems handle TCP port forwards fine but silently drop UDP, or the forward was never created for UDP.
  • ASPF or a DDoS blacklist is eating it. Application-layer gateways that inspect SIP sometimes drop packets that don't fit expected patterns. DDoS protection lists can flag UDP 7100 as suspicious traffic, especially from an unrecognized source.

Both failure modes produce the same signature: the packet dies before NAT, so the counter stays at zero.

Why "telnet works" proves nothing

Every ticket has a version of this line: "But I can telnet to port 7100 from outside, so the port is open."

No. Telnet is TCP. TCP 7100 has 13 hits — of course it works. The camera doesn't speak TCP on 7100; it speaks UDP. A TCP success tells you the ISP modem forwards TCP, the firewall allows TCP, and the NAT policy for TCP is intact. It tells you nothing about UDP.

This is the single most common false positive in network troubleshooting. TCP and UDP are separate protocols with separate connection tracking, separate NAT mappings, and separate policy rules. A port is not "open" or "closed" — a (protocol, port) pair is.

The fix is to stop testing with telnet and start testing with a probe that actually speaks the protocol in question.

A TCP + UDP port probe

Here's a script that turns "the port is broken" into a pass/fail matrix. It runs from outside the firewall and tests both TCP and UDP on the same port, then reports which combinations succeed.

#!/usr/bin/env python3
"""Probe TCP and UDP reachability on a target port.

Reports a pass/fail matrix so a vague complaint becomes a
reproducible result. Run from a host outside the firewall.
"""

import socket
import argparse
import time


def probe_tcp(host: str, port: int, timeout: float = 3.0) -> bool:
    """Return True if a TCP connection completes."""
    try:
        with socket.create_connection((host, port), timeout=timeout):
            return True
    except (socket.timeout, ConnectionRefusedError, OSError):
        return False


def probe_udp(host: str, port: int, timeout: float = 3.0,
              payload: bytes = b"\r\n\r\n") -> str:
    """Send a UDP datagram and classify the response.

    Returns one of: 'reply', 'icmp-unreachable', 'timeout'.
    A timeout is ambiguous: the packet may have been dropped, or
    the service may simply not reply to this payload. An ICMP
    port-unreachable means the packet reached a host and was
    rejected at the transport layer.
    """
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.settimeout(timeout)
    try:
        sock.sendto(payload, (host, port))
        try:
            data, _ = sock.recvfrom(4096)
            return "reply" if data else "reply"
        except socket.timeout:
            return "timeout"
        except ConnectionRefusedError:
            return "icmp-unreachable"
    finally:
        sock.close()


def main() -> None:
    ap = argparse.ArgumentParser(description="TCP/UDP port probe")
    ap.add_argument("host")
    ap.add_argument("port", type=int)
    ap.add_argument("--timeout", type=float, default=3.0)
    args = ap.parse_args()

    tcp_ok = probe_tcp(args.host, args.port, args.timeout)
    udp_result = probe_udp(args.host, args.port, args.timeout)

    print(f"Target: {args.host}:{args.port}")
    print(f"  TCP: {'PASS' if tcp_ok else 'FAIL'}")
    print(f"  UDP: {udp_result.upper()}")

    if tcp_ok and udp_result == "timeout":
        print("\nDiagnosis: TCP reaches the target, UDP does not.")
        print("Check the ISP modem for a UDP port forward, and")
        print("inspect ASPF / DDoS policy on the firewall.")


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Run it against the public IP:

$ python3 probe.py 198.51.100.77 7100
Target: 198.51.100.77:7100
  TCP: PASS
  UDP: TIMEOUT

Diagnosis: TCP reaches the target, UDP does not.
Check the ISP modem for a UDP port forward, and
inspect ASPF / DDoS policy on the firewall.
Enter fullscreen mode Exit fullscreen mode

That output is a ticket-closer. It's specific, reproducible, and it points at the right layer.

One caveat on UDP probing: a timeout is ambiguous. The packet may have been dropped, or the service may simply not reply to an arbitrary payload. If you can, send a payload the service actually understands — a real SIP OPTIONS request for GB28181, for example — so a reply confirms reachability rather than just absence of an error.

A SIP-aware variant

For GB28181 specifically, a bare UDP probe may not get a reply even when the path is open. Send a real SIP OPTIONS and watch for a response:

import socket

def sip_options_probe(host: str, port: int, timeout: float = 5.0) -> bool:
    """Send a SIP OPTIONS request and wait for any response."""
    msg = (
        f"OPTIONS sip:{host} SIP/2.0\r\n"
        f"Via: SIP/2.0/UDP 0.0.0.0:{port};branch=z9hG4bK-probe\r\n"
        f"From: <sip:probe@localhost>;tag=probe\r\n"
        f"To: <sip:{host}>\r\n"
        f"Call-ID: probe-{int(time.time())}\r\n"
        f"CSeq: 1 OPTIONS\r\n"
        f"Max-Forwards: 70\r\n"
        f"Content-Length: 0\r\n\r\n"
    ).encode()

    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.settimeout(timeout)
    try:
        sock.sendto(msg, (host, port))
        data, _ = sock.recvfrom(4096)
        return b"SIP/2.0" in data
    except socket.timeout:
        return False
    finally:
        sock.close()
Enter fullscreen mode Exit fullscreen mode

If this returns False while TCP 7100 is open, you've confirmed the UDP path is broken before it reaches the firewall. If it returns True, the path is fine and the problem is elsewhere — the platform's INVITE routing, the camera's registration binding, or the firewall's SIP ALG mangling headers.

Reading the counters correctly

The general lesson: a zero hit counter on a NAT rule doesn't mean the rule is wrong — it means no packet reached it. Compare the counter against the session table. If sessions exist but the counter is zero, the traffic that created those sessions never traversed the rule in the direction you're checking.

Build a habit of asking three questions for any "port is broken" report:

  1. What does the session table show? Is there an active session? What's the translation?
  2. What do the policy hit counters show, per protocol? A TCP hit count next to a zero UDP hit count is a specific diagnosis, not a coincidence.
  3. What does a protocol-aware probe show from outside? Telnet is not a UDP test. Ever.

The GB28181 case resolved when the ISP modem's UDP 7100 forward was added. The firewall was never the problem — the NAT rule had been translating heartbeats correctly the whole time. The counter was telling the truth; it just took a while to read it.

More notes like this ship every week on this site.


Daily Picks

The following pairs are selected from the multi-timeframe trend scanner (Gate.io futures) and are for technical-analysis study only — not investment advice.
Data updated: 2026-10-09 08:46:31

Short

Pair Signal Price Take Profit Stop Loss R/R
COOL 短平快做空 $0.0015 $0.0014 $0.0015 1:1.2

1 picks selected. Scanner runs every 15 minutes.

Top comments (0)