Skip to main content
RunBook Academy

← All break/fix scenarios in VyOS

advancedvyos-firewall~40 min

Firewall Conntrack Table Full

Reported symptoms

  • On weekday mornings from roughly 09:10, users report that the internet "works, then stops, then works again", and that reloading a page sometimes helps and sometimes does not; it clears around 09:50 and returns at 14:00
  • Two engineers disagree about whether the router is reachable: the one with an SSH session open finds it perfectly responsive, while the one trying to open a new session gets nothing until the client gives up
  • Nothing is down. Interface counters show no errors and no drops, CPU sits around 22%, BGP and OSPF are stable, and FRR has logged nothing
  • The firewall counters look healthy: the accept rules advance normally and the catch-all deny counter is flat, so the on-call engineer has ruled the firewall out
  • show log firewall shows nothing unusual for the affected time windows
  • The one check that flapped through the windows was a blackbox probe that opens a new TCP connection to an external endpoint; it was muted last month for being noisy
  • No change has been made to the router. The only change that week was on the server estate: the security team rolled out a new vulnerability scanner on Monday

Evidence

  • · cat /proc/sys/net/netfilter/nf_conntrack_count returns the same number as /proc/sys/net/netfilter/nf_conntrack_max during the affected windows, and roughly a tenth of it outside them
  • · sudo dmesg -T | grep nf_conntrack shows "table full, dropping packet" with timestamps that bracket each reported window exactly
  • · sudo conntrack -S shows non-zero and rising insert_failed and drop counters on every CPU
  • · sudo conntrack -L | grep -c SYN_SENT accounts for the overwhelming majority of the table
  • · Grouping the table by original source shows one host, 10.30.0.40, holding almost all of those half-open entries
  • · show firewall shows the chain counters advancing normally, with the default-deny counter no busier than on a quiet day
  • · The conntrack table size was set once, years ago, against a flow count that was measured at the time
  • · Existing flows are untouched throughout: an SSH session opened before the window stays responsive across it
Diagnosis and resolutionclick to reveal

Root cause

The connection tracking table reached its configured maximum, and when it is full the kernel drops the packets that would have created new entries rather than admitting them untracked. That is why the failure is so precisely shaped: a flow that already has an entry is fast-pathed and completely unaffected, while every new connection — a new SSH session, a new page load, a new probe — is discarded before any firewall rule is consulted. The firewall's own accounting cannot see it. Conntrack sits ahead of the filter chain in the packet path, so a packet dropped for want of a table slot never reaches a rule and never increments a counter, which is exactly why the on-call engineer ruled the firewall out on good evidence and was still looking at the wrong subsystem. What consumed the table was not user traffic. A vulnerability scanner deployed on Monday sweeps the estate on weekday mornings and again after lunch, and every probe it sends to a port that does not answer leaves a half-open flow parked in SYN_SENT until that timeout expires. A sweep of a few thousand probes a second therefore parks hundreds of thousands of entries that will never complete, and the windows end when the sweep does and the entries age out. Underneath both of those sits an assumption with no owner: the table size was set once against a measured flow count, that measurement was accurate when it was taken, and nothing re-measured it when the estate acquired a new workload whose entire purpose is to open connections that do not complete.

Remediation

Find the consumer before pulling any lever, because it decides which lever is right and it costs one command. Grouping the table by source resolves this in under a minute and tells you whether you are looking at a capacity problem or at one host. Here it is one host, so the cheapest and fastest fix is to stop or reshape the sweep — pause it, rate-limit it, or scope it so it does not cross the stateful edge — which is not your change to make and needs a named owner in the security team rather than a message in a channel. Raising the table with set system conntrack table-size restores service in a single commit and is the right thing to do while the conversation happens, provided it is recorded as a hold and not as the fix: each entry costs roughly 300 bytes of kernel memory, and raising the ceiling under a driver you have not addressed buys time while making the next failure later, larger and harder to recognise. Shortening the timeout that is parking the entries is a genuine lever and has a genuine cost — a legitimate connection to a slow or briefly unreachable destination can lose its state before the retransmission that would have completed it. The structural fix is a per-source connection cap, so that no single host can consume the table whatever it is doing; choose the limit and the mask from measured behaviour, because a cap set by guesswork will eventually be hit by a proxy or a relay that is behaving perfectly. Whatever is chosen as a hold gets an owner and an end date written down, because a raised ceiling with nobody assigned to it is how this recurs at a size where raising it again is no longer possible.

Verification

Verify across a full scan window, not on a quiet afternoon, because everything passes on a quiet afternoon. Through the window, nf_conntrack_count must peak with visible headroom below nf_conntrack_max rather than pinning at it, and the insert_failed and drop counters in sudo conntrack -S must stay flat from the start of the window to the end — a single sample proves nothing, since the failure is intermittent by construction. sudo dmesg -T must produce no new table-full lines. Then reproduce the exact thing that failed: open a new SSH session from the jump host in the middle of a window, rather than checking that the session you already have still works, which it did throughout the incident. Unmute the blackbox probe that opens a new TCP connection and require it to stay green across a full window, since that probe was the only check that ever saw this. If a per-source cap was added, prove it can fire by exceeding it deliberately from a test host, and prove it does not fire for the legitimate high-fan-out hosts by watching them through a busy period.

Prevention

Monitor the ratio of nf_conntrack_count to nf_conntrack_max rather than the count, which means nothing on its own, and alert while there is still headroom to act — somewhere around three quarters is early enough to be useful. Give this failure its own signal, because it is invisible everywhere an operator normally looks: the firewall counters cannot see it, the interface counters cannot see it, and CPU stays unremarkable throughout. The insert_failed and drop counters from conntrack and the kernel log forwarded to central syslog are the two places it is visible at all. Size the table from a measured peak with room for an abnormal day, and re-measure when the estate acquires a workload that changes the shape of its traffic; a number that was right when it was set is not a number that stays right. Set the protocol timeouts to what the traffic actually does instead of leaving defaults that were chosen for a general-purpose host. Cap per-source consumption structurally, so that a single misbehaving or merely enthusiastic host cannot take the whole table: that turns an estate-wide outage into one host's problem, which is the difference between an incident and a ticket. Treat "who may open flows through the stateful edge" as a policy question that is answered before a scanning tool is deployed rather than after it, and give the answer to whoever owns the tool. Finally: the one probe that detected this had been muted for being noisy. A check that flaps in a pattern is not noise, it is a signal nobody has read yet, and muting it removed the only warning the estate had.

Reported symptoms

The complaints started on Tuesday and nobody could make them repeat on demand.

Users on the DC-East office network say the internet “works, then stops, then works again”. It goes wrong somewhere around 09:10, clears by about 09:50, and comes back after lunch. Reloading a page helps sometimes and does nothing other times. Nobody can produce a failing example while someone is watching.

The router looks fine, and it looks fine in every way an operator normally checks:

  • CPU around 22%, no interface errors, no interface drops, no queue buildup.
  • BGP and OSPF stable, no adjacency changes, nothing from FRR in the logs.
  • The firewall’s accept rules are advancing at their usual rate and the catch-all deny counter is no busier than on a quiet day. On that evidence the on-call engineer ruled the firewall out, which was a reasonable conclusion from a real observation.
  • show log firewall shows nothing unusual in the affected windows.

Two things in the incident channel should have been the whole diagnosis and were read as noise instead.

First, two engineers disagreed about whether the router was reachable. One had an SSH session already open and found it completely responsive. The other, opening a new session at the same moment, got nothing until the client gave up. They spent ten minutes deciding one of them had a local network problem.

Second, a blackbox probe that opens a new TCP connection to an external endpoint had been flapping for two days. It was muted last month for being noisy.

Nothing has been changed on the router. The only change on the estate that week is a vulnerability scanner the security team rolled out on Monday.

Evidence provided

Read-only / Safetaken at 09:31 — the table is not nearly full, it is exactly full
$ cat /proc/sys/net/netfilter/nf_conntrack_count /proc/sys/net/netfilter/nf_conntrack_max
262144
262144

Illustrative output

Read-only / Safethe kernel has been saying so since 09:11
$ sudo dmesg -T | grep nf_conntrack | tail -3
[Tue Aug 18 09:11:44 2026] nf_conntrack: nf_conntrack: table full, dropping packet
[Tue Aug 18 09:11:44 2026] nf_conntrack: nf_conntrack: table full, dropping packet
[Tue Aug 18 09:11:45 2026] nf_conntrack: nf_conntrack: table full, dropping packet

Illustrative output

Read-only / Safeinsert_failed and drop are the counters nothing else exposes
$ sudo conntrack -S
cpu=0 found=0 invalid=214 insert=0 insert_failed=8814 drop=8814 early_drop=0 error=0 search_restart=1902
cpu=1 found=0 invalid=190 insert=0 insert_failed=9022 drop=9022 early_drop=0 error=0 search_restart=1877

Illustrative output

Read-only / Safeof 262144 entries, almost all are half-open
$ sudo conntrack -L 2>/dev/null | grep -c SYN_SENT
241377

Illustrative output

Read-only / Safeone host holds 91% of the table
$ sudo conntrack -L 2>/dev/null | awk '/^tcp/{print $5}' | sort | uniq -c | sort -rn | head -4
239104 src=10.30.0.40
 1183 src=10.30.0.61
  908 src=10.30.0.62
  644 src=203.0.113.61

Illustrative output

Read-only / Safethe firewall is not dropping anything unusual, and that is true
$ show firewall
Rule     Action   Packets     Bytes   Description
----     ------   -------     -----   -----------
10       accept   3184402     441M    Established/related
20       drop     37          2.9K    Invalid
30       accept   1204        88K     SSH from jump host
9999     drop     14022       841K    Default deny

Illustrative output

Read-only / Safethe physical and link layers are clean
$ show interfaces

Work the evidence before reading on

The hardest part of this incident is that a correct observation sent the investigation the wrong way. Work out which one.

  1. An SSH session opened before 09:10 is responsive throughout the window, and a new one cannot be established. What is different about the two from the router’s point of view, and which subsystem knows that difference?
  2. The firewall counters are normal and the catch-all deny is flat. If the packets are being discarded, and the firewall is not the thing discarding them, what else in the packet path is in a position to?
  3. 241,377 of 262,144 entries are in SYN_SENT. What kind of traffic produces a half-open flow, in bulk, that never progresses — and what does it tell you that 91% of them share one source?
  4. The windows start and end at the same times on consecutive weekdays. No router change was made. What kind of cause has a schedule?

Before continuing: at what point in the packet path is a packet dropped, if the drop leaves no trace in any firewall counter and no line in the firewall log?

Root cause

1. A full table drops the packets that would create entries

Connection tracking is a fixed-size hash table in kernel memory. When it is full, the kernel does not evict, does not queue and does not admit the packet untracked — it drops the packets that would have created new entries.

That produces a very specific failure shape, and the shape is the diagnosis. A flow that already has an entry is matched, fast-pathed and entirely unaffected. A flow that does not — a new SSH session, a new page load, a new probe — is discarded. Everything that was working keeps working, and nothing new can start.

This is why two engineers on the same router disagreed about whether it was up. One was asking a question the table already had an answer to, and the other was asking for a new entry there was no room to create.

2. The drop happens before the firewall, so the firewall cannot report it

Conntrack sits ahead of the filter chains in the packet path. A packet discarded for want of a table slot never reaches a rule, so it increments no rule counter and produces no log line, including from the default-deny rule and its logging.

The on-call engineer looked at the firewall counters, saw nothing anomalous, and concluded the firewall was not involved. That observation was completely correct and the conclusion drawn from it was not: the firewall really was not dropping this traffic, and the subsystem that was sits one hop earlier and keeps its accounting somewhere else entirely.

conntrack -S is where that accounting lives. insert_failed and drop are the only counters on the box that move when this happens, and nothing routinely graphs them.

3. What filled the table was traffic that is never meant to complete

The scanner deployed on Monday sweeps the estate on weekday mornings and again after lunch. Every probe it sends to a port that does not answer leaves a flow parked in SYN_SENT until that state times out, and the default is generous because it is chosen to survive a slow handshake rather than to survive a sweep.

At a few thousand probes a second, that parks hundreds of thousands of entries which will never progress and never be cleaned up early. The windows end when the sweep does and the parked entries age out over the following couple of minutes, which is exactly the recovery pattern the users described.

User traffic was never the problem. The table was consumed by flows that had no intention of becoming connections.

4. Underneath all of it, a number nobody owned

The table size was set once, against a flow count measured at the time, and that measurement was correct when it was taken. Nothing re-measured it when the estate acquired a workload whose entire purpose is to open connections that do not complete.

That is the part worth carrying away, because it is not specific to conntrack. A capacity figure derived from a measurement is only as current as the measurement, and nothing about it announces when it has gone stale.

Resolution

  1. Identify the consumer before touching any setting. Grouping the conntrack table by source takes one command and under a minute, and it decides everything that follows: one host holding 91% of the table is a completely different problem from a table that filled evenly.
  2. Restore service. Raising the table size with set system conntrack table-size takes effect on commit and is reversible, which makes it the right immediate action — recorded in the incident as a hold, with an owner and a date, not as the resolution.
  3. Take the driver to its owner. Pausing, rate-limiting or rescoping the sweep so that it does not cross the stateful edge is the cheapest real fix available, and it is a change in the tool owned by the security team rather than in your router. A message in a channel is not an owner.
  4. Consider the timeout that is parking the entries, and price it honestly. Shortening the state that half-open flows sit in frees the table faster, and it also means a legitimate connection to a slow or briefly unreachable destination can lose its state before the retransmission that would have completed it.
  5. Add the structural control: a per-source connection cap, so that no single host can consume the table whatever it is doing. Size the limit and the mask from measured behaviour — a guessed cap will eventually be hit by a proxy or a mail relay that is behaving perfectly, and that failure will look exactly like this one.
  6. Re-measure the baseline once the estate is quiet, and set the table size from that measurement plus room for an abnormal day, rather than leaving whatever number the incident happened to end on.

Verification

  1. Verify across a whole scan window, not on a quiet afternoon. Every check in this list passes at 16:00 on a Friday and none of them mean anything there.
  2. The count peaks with headroom. Through the window, nf_conntrack_count must stay visibly below nf_conntrack_max rather than pinning at it.
  3. The failure counters stay flat. sudo conntrack -S must show insert_failed and drop unchanged from the start of the window to the end, and sudo dmesg -T must produce no new table-full lines.
  4. Reproduce the exact thing that failed. Open a new SSH session from the jump host in the middle of a window. Confirming that an existing session still works reproduces the symptom that misled everybody, not the fault.
  5. Unmute the blackbox probe that opens a new TCP connection, and require it green across a full window. It was the only check that ever saw this, and it is the check that tells you whether the fix held next month.
  6. If a per-source cap was added, prove it fires by exceeding it deliberately from a test host, and prove it does not fire for legitimate high-fan-out hosts by watching them through a busy period. An untested cap is an outage waiting for a mail relay.

Prevention

  • Alert on the ratio of nf_conntrack_count to nf_conntrack_max, not on the count. The count is meaningless without the maximum, and the ratio needs to fire while there is still headroom to act — around three quarters is early enough to be useful and late enough not to be noise.
  • Give this failure its own signal. The firewall counters, the interface counters and the CPU graph are all blind to it by construction. The kernel ring buffer and conntrack -S are where it is visible; forward the kernel log to central syslog and graph the two counters.
  • Size the table from a measured peak with room for an abnormal day, and re-measure whenever the estate acquires a workload that changes the shape of its traffic. A capacity number is only as current as the measurement behind it.
  • Set the protocol timeouts to what the traffic actually does. The defaults are chosen for a general-purpose host, not for an edge router carrying an estate’s flow table.
  • Cap per-source consumption structurally. A per-source connection limit turns “the estate is down” into “one host is being refused”, which is the difference between an incident and a ticket.
  • Decide who may open flows through the stateful edge before the tool that opens them is deployed. A scanner sweeping through a stateful device is a capacity decision, and the person who owns the scanner is usually not the person who finds out.
  • Treat a flapping check as unread signal rather than as noise. The one probe that detected this was muted a month before the incident, for behaving exactly as it should have.