Stateful vs stateless firewalls, cloud NSGs/security groups, NACLs, WAFs, Kubernetes NetworkPolicy, idle timeouts, and a step-by-step connectivity troubleshooting method.
A firewall allows or blocks traffic based on rules (source, destination, port, protocol, and for L7 firewalls/WAFs, HTTP content).
Troubleshoot by symptom: timeout usually means dropped by a firewall or routing, refused means reached the host but nothing listening, reset after idle often means a firewall or load balancer idle timeout removed the connection state. Test from the source (nc -vz, Test-NetConnection), check rules and flow logs in both directions, and set TCP keep-alives or shorter pool idle timeouts below the firewall's idle timeout.
Stateful (NSG, security group):
outbound app → db:1433 allowed ──► connection tracked
reply db:1433 → app:51544 ──► allowed automatically (matches tracked flow)
Stateless (NACL):
outbound rule app → db:1433
inbound rule db:1433 → app:1024-65535 ← must be added, or replies are dropped
| Layer | Examples | Scope |
|---|---|---|
| Edge / L7 | WAF, Front Door, CloudFront, API gateway | HTTP rules, rate limits |
| Network | Azure Firewall, NVA, AWS Network Firewall | Central egress/ingress |
| Subnet / NIC | NSG, security group, NACL | Per subnet or interface |
| Cluster | Kubernetes NetworkPolicy, service mesh policy | Pod-to-pod |
| Host | iptables/nftables, Windows Firewall | Per machine |
| Service | Database firewall rules, private endpoint settings | Per resource |
nc -vz host port or Test-NetConnection host -Port 1433.tcpdump -i any host 10.0.2.4 and port 1433 to see SYNs without replies.A .NET worker called a partner's SFTP and HTTPS endpoints. It worked during the day but the first call after a quiet night failed with IOException: Unable to read data from the transport connection: Connection reset by peer. The Azure NAT gateway's default idle timeout is 4 minutes; idle pooled connections had been silently dropped, and the next write on them was reset. We set SocketsHttpHandler.PooledConnectionIdleTimeout to 2 minutes, enabled TCP keep-alive for the SFTP client, and added a retry for idempotent calls. The resets stopped.
Checks practical network security knowledge and a structured troubleshooting approach.
Green flags
Red flags
Timeout: SYN ────► (dropped) retry SYN ────► (dropped) ... → SocketException TimedOut
Refused: SYN ────► host
◄──── RST (no listener) → ConnectionRefused
Reset: established ... idle ... firewall forgets flow
data ────► middlebox / server: unknown connection
◄──── RST → ConnectionReset
Some firewalls send RST instead of dropping ("reject" vs "drop"), so refused can also come from a firewall. Check the source IP of the RST in a capture.
| Component | Typical default idle timeout |
|---|---|
| Azure Load Balancer / NAT Gateway | 4 minutes (configurable) |
| AWS NAT Gateway | 350 seconds |
| AWS NLB | 350 seconds |
| Corporate firewalls | often 30–60 minutes |
| Linux conntrack established | 5 days (rarely the limit) |
TCP keep-alive by default on Linux starts after 2 hours, far longer than these. Options: shorter pool idle timeouts in clients, application-level pings, or TCP keep-alive with shorter intervals (SocketsHttpHandler.KeepAlivePingDelay is for HTTP/2 pings; for raw sockets set SocketOptionName.TcpKeepAliveTime).
Many organisations force outbound traffic through a central firewall with FQDN allow-lists. New dependencies (a SaaS API, a package feed, a telemetry endpoint) then fail with timeouts until rules are added. Keep a documented list of required egress endpoints per service.
nc -vz db.internal 1433 # Linux/macOS
curl -v telnet://db.internal:1433 # if nc is missing
Test-NetConnection db.internal -Port 1433 # Windows PowerShell
tcpdump -ni any host 10.0.2.4 and port 1433
kubectl run tmp --rm -it --image=nicolaka/netshoot -- bash # debugging pod with tools
iptables -A INPUT -p tcp --dport 8080 -j DROP vs -j REJECT on a test VM and compare the client errors.HttpClient connection through a NAT with a short idle timeout and observe the reset on the next request.// Keep pooled connections shorter-lived than middlebox idle timeouts
builder.Services.AddHttpClient<PartnerClient>()
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2), // below a 4-minute NAT idle timeout
PooledConnectionLifetime = TimeSpan.FromMinutes(10),
ConnectTimeout = TimeSpan.FromSeconds(5) // fail fast on dropped SYNs
})
.AddStandardResilienceHandler(); // retries idempotent requests with backoff
// Raw socket: enable TCP keep-alive with short intervals (.NET Core 3.0+)
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, 60); // seconds idle
socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, 15); // between probes
socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, 4);
await socket.ConnectAsync("sftp.partner.com", 22, ct);
# NetworkPolicy: default-deny egress but allow DNS and the database
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: orders-egress, namespace: orders }
spec:
podSelector: { matchLabels: { app: orders-api } }
policyTypes: [Egress]
egress:
- to:
- namespaceSelector: {}
podSelector: { matchLabels: { k8s-app: kube-dns } }
ports: [{ protocol: UDP, port: 53 }, { protocol: TCP, port: 53 }]
- to:
- ipBlock: { cidr: 10.20.4.0/24 } # database subnet
ports: [{ protocol: TCP, port: 1433 }]
Mistake: Testing connectivity from your laptop instead of the failing workload.
Why it is wrong: Your laptop has different routes, DNS answers and firewall rules, so a successful test proves nothing about the pod or VM.
Instead: Run nc -vz, curl -v or Test-NetConnection from the same pod, VM or subnet, for example with kubectl debug or a netshoot pod.
Mistake: Treating every connection failure as "the firewall". Why it is wrong: Refused usually means nothing is listening, and TLS or HTTP errors mean the network path already works. Instead: Classify the error (timeout, refused, reset, TLS, HTTP) and look at the matching layer.
Mistake: Forgetting return traffic with stateless rules. Why it is wrong: NACLs drop replies to ephemeral ports unless an inbound rule allows them, so outbound calls time out. Instead: Know which controls are stateful, and add ephemeral-port return rules for stateless ones.
Mistake: Adding a default-deny egress policy without allowing DNS. Why it is wrong: Every name lookup times out and the whole service fails, which looks like a database or API outage. Instead: Always include UDP and TCP 53 to the cluster DNS, and test the policy in a lower environment first.
Mistake: Ignoring idle timeouts on NAT gateways and load balancers.
Why it is wrong: Pooled connections die silently after 4 to 6 minutes idle and the next request gets a reset.
Instead: Set PooledConnectionIdleTimeout or TCP keep-alive below the shortest middlebox timeout and retry idempotent calls.
Connectivity debugging is about narrowing the layer quickly and proving it with data rather than guesses.
| Symptom | Most likely layer | Evidence to collect |
|---|---|---|
| Connect timeout | Firewall, NSG, route, wrong IP from DNS | Flow logs showing deny, tcpdump with SYNs and no SYN-ACK |
| Connection refused | Destination has no listener, or a reject rule | ss -ltnp on target, source IP of the RST |
| Reset after idle | NAT or load balancer idle timeout | Correlation with idle duration, middlebox settings |
| Works by IP, fails by name | DNS or private DNS zone | dig from the workload |
| TLS or HTTP error | Path fine; certificate or app problem | openssl s_client, app logs |
I design for middleboxes: clients recycle idle connections, fail fast on connect and retry idempotent operations. I treat network rules as code (Terraform or Bicep, NetworkPolicies in the repo), keep each service's required egress documented, and monitor connection reset and timeout rates alongside flow-log denies, so a new dependency is a reviewed change rather than a 2 am discovery.
A port is a 16-bit number (0 to 65535) that lets one IP address host many services. A socket is the OS object for one endpoint: a listening socket bound to IP:port, and a connected socket for each accepted connection, identified by the 4-tuple (client IP, client port, server IP, server port).
The Docker problem is almost always binding: the app listens on localhost (127.0.0.1) inside the container, which is unreachable from outside its network namespace, or it listens on a different port from the one you mapped. The official .NET 10 images set ASPNETCORE_HTTP_PORTS=8080, which binds all interfaces on 8080 (the default since .NET 8), so map it with -p 5000:8080 and do not override it with a localhost URL.
DNS maps names to records: A (IPv4), AAAA (IPv6), CNAME (alias), plus MX, TXT, SRV, NS and PTR. A client asks a recursive resolver, which walks root → TLD → authoritative servers and caches the answer for its TTL. .NET uses the OS resolver (getaddrinfo) and does not cache itself.
Typical incidents:
HttpClient kept pooled connections to the old IP. Fix with PooledConnectionLifetime.ndots:5 makes each external name try several search domains first, and UDP conntrack races cause occasional 5-second delays.Diagnose with dig/nslookup from the same pod, check TTLs, and look at the DNS phase of the HTTP timing.
Layered from outside in:
Plus observability at every hop (flow logs, edge logs, traces with traceparent), and automated certificates and network rules as code.
How to tell it (STAR)
**S:** Our [partner integration] failed on the first call every morning with connection resets. **T:** I was asked to make it reliable. **A:** The failures only happened after more than [four] minutes of idle time, which matched the NAT gateway's idle timeout. Our pooled connections outlived it, so the first request used a dead connection. I lowered the pool idle timeout, enabled keep-alives for SFTP and added retries for idempotent calls. **R:** Morning failures went to [zero], and the shared client defaults now prevent it in other services.
Last updated 23 Oct 2026.