Fix monit api iptables rules to prevent LAST-ACK - #705
Conversation
Commit 4dfe37a refactored the iptables rules in restrict-monit-api-access to be idempotent, but accidentally reversed the insertion order. By using `-I` (Insert at position 1) for both rules, the DROP rule was inserted first, and then the ACCEPT rule was inserted above it. This meant the DROP rule was evaluated before the ACCEPT rule. As a result, TCP RESET packets sent by the kernel (which do not belong to the monit-api-access cgroup) to clean up orphaned connections were dropped. This left sockets lingering in the LAST-ACK state for ~106 seconds, causing subsequent monit commands to fail with "Cannot connect to the monit daemon" if they were assigned the same ephemeral port. This commit fixes the idempotency logic to ensure the rules are always inserted in the correct order, using `-A` to append so that the order is the same as the reading order (ACCEPT evaluated before DROP).
WalkthroughThe script now documents the required ordering of loopback Monit firewall rules. The 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@stemcell_builder/stages/bosh_monit/assets/restrict-monit-api-access`:
- Around line 27-36: Update the managed iptables rule logic in
restrict-monit-api-access so each run guarantees the ESTABLISHED,RELATED ACCEPT
rule precedes the cgroup-based DROP rule, rather than only checking rule
presence. Remove and re-add the managed DROP rule or rebuild both managed rules
in the correct order, while preserving idempotency; add coverage for
pre-existing reversed and DROP-only states.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 1e53023c-09ae-47dc-b63a-aa20e97c2d89
📒 Files selected for processing (1)
stemcell_builder/stages/bosh_monit/assets/restrict-monit-api-access
|
Confirmed on a candidate stemcell that these changes result in the correct order of the rules: |
Description
This PR fixes a regression introduced in commit
4dfe37a31(Add cgroups v2 support for Jammy stemcells) that causes intermittentCannot connect to the monit daemonerrors during BOSH deployments.The Bug:
The previous commit refactored the
iptablesrules inrestrict-monit-api-accessto be idempotent, but accidentally reversed the insertion order. Both rules used-I(insert at position 1), which caused theDROPrule to be evaluated before theESTABLISHED,RELATED -j ACCEPTrule.Because of this, TCP RESET packets sent by the kernel to clean up orphaned connections were dropped (since the kernel does not belong to the
monit-api-accesscgroup). This left sockets lingering in theLAST-ACKstate for ~106 seconds. If a subsequentmonitcommand was assigned the same ephemeral port, it would fail with a "Connection Refused" error.This results in deployments failing with:
We can observe the
LAST-ACKconnections in a real-vm deployment scenario. In certain CI environments, this makes the deployment fail.The Fix:
This PR updates the script to insert the rules in the correct order
Testing
Verified on a live VM that:
iptablesrules are now in the correct order (ACCEPTis rule 1,DROPis rule 2).bosh stopandbosh startin a loop, and observed several connections stuck inLAST-ACKthat resulted in some failures (watch "ss -tan '( sport = :2822 or dport = :2822 )'")AI analysis of why this happens
Here is the exact sequence of events that causes the kernel to send the TCP RST (which then gets dropped by the firewall):
When the BOSH agent polls monit (e.g., during StopAndWait), it makes an HTTP GET request to http://127.0.0.1:2822/_status2.
Because it uses Go's default http.Client, it uses HTTP Keep-Alive. After the BOSH agent receives the XML response, it does not close the TCP connection. Instead, the Go runtime puts that idle TCP connection into a connection pool, hoping to reuse it for the next poll 500ms later.
monit is a very simple HTTP server. It does not support HTTP Keep-Alive by default, or it has a very aggressive timeout for idle connections.
Shortly after sending the XML response, the monit daemon decides it is done with the transaction and initiates a TCP connection close.
monit sends a TCP FIN packet to the BOSH agent.
The BOSH agent's OS kernel receives the FIN, sends an ACK, and puts the socket into CLOSE-WAIT.
The monit daemon's socket goes into FIN-WAIT-2.
3. The Client Discovers the Dead Connection
Meanwhile, the Go runtime inside the BOSH agent still thinks this connection is sitting happily in its idle connection pool.
When the BOSH agent wakes up 500ms later to poll monit again, the Go runtime pulls that connection out of the pool and tries to write the new HTTP GET request to it.
When the Go runtime tries to write data to a socket that the server has already started closing (the socket is in CLOSE-WAIT), the Linux kernel immediately realizes the connection is invalid.
The kernel's TCP stack reacts by aborting the connection. It generates a TCP RST (Reset) packet and sends it to monit to say, "Abort! The application tried to write to a closed pipe!"
Because this RST packet is generated asynchronously by the kernel's TCP stack in response to an invalid write, the packet is not tagged with the BOSH agent's cgroup (monit-api-access).
The packet hits the iptables rules. Because the rules are out of order (the DROP rule is evaluated first), the firewall drops the RST packet.
Because the RST packet was dropped, the monit daemon never receives it.
monit eventually sends its final FIN packet, transitioning its socket to LAST-ACK, waiting for an acknowledgment that will never come (because the firewall is dropping it). The socket sits there for ~106 seconds until the kernel's TCP retransmission timer finally gives up and clears it.