Docs: the real DSM fix is bridge-nf-call-iptables=0, not an iptables rule

Confirmed on the operator's NAS. The earlier advice (insert an ACCEPT rule
ahead of DSM's chain) does not hold: DSM reloads its firewall periodically,
flushing and rebuilding the chains. You can watch FORWARD_FIREWALL's DROP
counter reset to zero and the inserted rule vanish with it, having matched no
packets. That is also why the symptom looked intermittent — 60/40 success on
repeated connections, with RELATED,ESTABLISHED keeping existing flows alive.

The fix is one level down: net.bridge.bridge-nf-call-iptables=0 takes
same-bridge traffic out of iptables entirely, so DSM's reloading cannot affect
it. Persisted with a Boot-up Triggered Task.

Section 8 now also answers "is that safe", accurately: the flag affects only
packets bridged within one network. Traffic between different Docker networks
is routed, so DOCKER-ISOLATION still applies; inbound traffic to published
ports is still routed and still firewalled; INPUT, which protects the NAS
itself, is untouched. The one real caveat is that it is host-wide, so VMs
sharing a bridge lose same-bridge filtering too.

The migrate failure message points at the sysctl now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nux3LCu4zP9nih4BRhPyHw
This commit is contained in:
2026-09-14 21:21:22 +02:00
co-authored by Claude Opus 5
parent d29c46c229
commit cc63efa968
2 changed files with 67 additions and 39 deletions
+4 -4
View File
@@ -72,10 +72,10 @@ print(
print( print(
"[entrypoint] If this is a Synology NAS: DSM's firewall filters traffic " "[entrypoint] If this is a Synology NAS: DSM's firewall filters traffic "
"between containers (FORWARD -j FORWARD_FIREWALL runs before Docker's own " "between containers (FORWARD -j FORWARD_FIREWALL runs before Docker's own "
"rules and denies by default). Fix:\n" "rules and ends in DROP), and it reloads periodically, so an added iptables "
" iptables -I FORWARD 1 -i docker+ -o docker+ -j ACCEPT\n" "rule does not survive. Fix:\n"
"See docs/nas-install.md, 'DSM firewall blocks container-to-container " " sysctl -w net.bridge.bridge-nf-call-iptables=0\n"
"traffic'.", "See docs/nas-install.md section 8.",
file=sys.stderr, file=sys.stderr,
) )
sys.exit(1) sys.exit(1)
+63 -35
View File
@@ -401,60 +401,88 @@ backends, or search ever looks stale, just rebuild it:
--- ---
## 8. DSM firewall blocks container-to-container traffic ## 8. DSM firewall breaks container-to-container traffic
**Symptom:** everything looks healthy, but `migrate` fails with an asyncpg **Symptom:** everything reports healthy, but `migrate` fails with an asyncpg
`TimeoutError`, or any service times out reaching `postgres:5432`. From a `TimeoutError`, or services intermittently cannot reach `postgres:5432`. Testing
throwaway container the port may even test open — because already-established by hand is maddening — the same command succeeds, then fails, then succeeds:
flows are allowed while new ones are dropped.
**Cause:** if Synology's firewall is enabled, DSM inserts its own chain ahead of
Docker's:
``` ```
-A FORWARD -j FORWARD_FIREWALL <- DSM firewall, runs first, denies by default postgres:5432 - no response
postgres:5432 - accepting connections
postgres:5432 - no response
```
**Cause:** with Synology's firewall enabled, DSM puts its own chain ahead of
Docker's in `FORWARD`:
```
-A FORWARD -j FORWARD_FIREWALL <- DSM's, first, ends in -j DROP
-A FORWARD -j DEFAULT_FORWARD <- Docker's ACCEPT rules, never reached -A FORWARD -j DEFAULT_FORWARD <- Docker's ACCEPT rules, never reached
``` ```
With `bridge-nf-call-iptables=1` (the DSM default), traffic between two and `net.bridge.bridge-nf-call-iptables=1` (the DSM default) forces traffic
containers on the same bridge traverses `FORWARD` — so it hits DSM's firewall, between two containers *on the same bridge* through that chain. Your database
matches no allow rule (your database port is not in DSM's service list, and port is not in DSM's allow list — and should not be — so new connections are
should not be), and is dropped before Docker's dropped. The `RELATED,ESTABLISHED` accept near the top is what makes it look
`-i docker-xxx -o docker-xxx -j ACCEPT` is consulted. intermittent rather than broken: flows that got established keep working.
**Fix** — allow Docker bridges to talk to themselves, ahead of the firewall: Adding an iptables rule does **not** hold. DSM reloads its firewall
periodically, which flushes and rebuilds the chains; you can watch the packet
counters reset to zero and any rule you inserted disappear with them.
**Fix — take bridge traffic out of iptables entirely:**
```bash ```bash
iptables -I FORWARD 1 -i docker+ -o docker+ -j ACCEPT sysctl -w net.bridge.bridge-nf-call-iptables=0
``` ```
`docker+` matches any interface whose name starts with `docker`, so it keeps Two containers on one bridge are a layer-2 conversation; that flag is what
working when the bridge is recreated under a new name. It permits traffic only forces it through a layer-3 firewall in the first place. With it off the packets
*between* Docker bridges — nothing from the LAN or the internet gains access. cross the bridge directly, where DSM's reloading cannot reach them.
**Make it survive a reboot.** The rule is lost on restart and when DSM reloads Verify — expect 10 out of 10:
its firewall. Add a *triggered* task in *Control Panel → Task Scheduler →
Create → Triggered Task → User-defined script*, event **Boot-up**, user ```bash
**root**: for i in $(seq 10); do
docker run --rm --network webmail-prod postgres:16 pg_isready -h postgres -p 5432 -t 5
done
```
**Make it permanent.** *Control Panel → Task Scheduler → Create → Triggered
Task → User-defined script*, event **Boot-up**, user **root**:
```bash ```bash
sleep 60 sleep 60
iptables -C FORWARD -i docker+ -o docker+ -j ACCEPT 2>/dev/null \ sysctl -w net.bridge.bridge-nf-call-iptables=0
|| iptables -I FORWARD 1 -i docker+ -o docker+ -j ACCEPT
``` ```
The `sleep` lets DSM's firewall finish loading first; `-C` checks for the rule Re-check it after a DSM upgrade with `sysctl net.bridge.bridge-nf-call-iptables`.
so running it twice is harmless. Re-run the same script by hand after any
change to the firewall settings.
**Verify it:** ### Is that safe?
```bash Yes, for this deployment, and the scope is narrower than it first sounds.
docker run --rm --network webmail-prod postgres:16 \
pg_isready -h postgres -p 5432 # expect: accepting connections
```
--- The flag only affects packets **bridged within a single network** — that is,
containers on the same Docker network talking to each other. That they can do so
is Docker's normal behaviour on any ordinary Linux host; DSM's default is the
unusual one.
What does **not** change:
- **Isolation between different Docker networks.** That traffic is *routed*, not
bridged, so it still traverses `FORWARD` and Docker's
`DOCKER-ISOLATION-STAGE-1/2` rules still apply.
- **Traffic from your LAN or the internet into a published port.** Still routed,
still filtered by DSM's firewall. The one published port here is `WEB_PORT`.
- **Traffic from containers out to the network.** Still routed and filtered.
- **DSM's own protection.** The `INPUT` chain, which guards the NAS itself, is
untouched.
The one thing to be aware of: it is a host-wide setting, so it applies to every
bridge on the box. If you run virtual machines on a bridged network *and* rely
on DSM's firewall to filter traffic between VMs on that same bridge, that
filtering stops too. Filtering of VM traffic to anywhere else is unaffected.
## 9. Troubleshooting ## 9. Troubleshooting
@@ -464,7 +492,7 @@ docker run --rm --network webmail-prod postgres:16 \
| UI loads, no live updates | WebSocket not enabled in the NAS reverse proxy | §5.2 Custom Header → WebSocket | | UI loads, no live updates | WebSocket not enabled in the NAS reverse proxy | §5.2 Custom Header → WebSocket |
| Login works, logs out on reload | HTTPS terminated upstream but `X-Forwarded-Proto` stripped | Use the NAS reverse proxy as documented; don't strip headers | | Login works, logs out on reload | HTTPS terminated upstream but `X-Forwarded-Proto` stripped | Use the NAS reverse proxy as documented; don't strip headers |
| "port is already allocated" | 8088 taken by another app | change `WEB_PORT` | | "port is already allocated" | 8088 taken by another app | change `WEB_PORT` |
| `migrate` fails with asyncpg TimeoutError | DSM firewall dropping container traffic (§8) | `iptables -I FORWARD 1 -i docker+ -o docker+ -j ACCEPT` | | `migrate` fails with asyncpg TimeoutError | DSM firewall dropping container traffic (§8) | `sysctl -w net.bridge.bridge-nf-call-iptables=0` |
| Mail not syncing | worker stopped, or credentials rejected | `./deploy/webmail.sh logs worker-sync` | | Mail not syncing | worker stopped, or credentials rejected | `./deploy/webmail.sh logs worker-sync` |
| Search finds nothing after a restore | index not rebuilt | `./deploy/webmail.sh reindex --purge` | | Search finds nothing after a restore | index not rebuilt | `./deploy/webmail.sh reindex --purge` |
| App won't start after editing `.env` | malformed `MASTER_KEY` | `./deploy/webmail.sh doctor` | | App won't start after editing `.env` | malformed `MASTER_KEY` | `./deploy/webmail.sh doctor` |