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:
+63
-35
@@ -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
|
||||
`TimeoutError`, or any service times out reaching `postgres:5432`. From a
|
||||
throwaway container the port may even test open — because already-established
|
||||
flows are allowed while new ones are dropped.
|
||||
|
||||
**Cause:** if Synology's firewall is enabled, DSM inserts its own chain ahead of
|
||||
Docker's:
|
||||
**Symptom:** everything reports healthy, but `migrate` fails with an asyncpg
|
||||
`TimeoutError`, or services intermittently cannot reach `postgres:5432`. Testing
|
||||
by hand is maddening — the same command succeeds, then fails, then succeeds:
|
||||
|
||||
```
|
||||
-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
|
||||
```
|
||||
|
||||
With `bridge-nf-call-iptables=1` (the DSM default), traffic between two
|
||||
containers on the same bridge traverses `FORWARD` — so it hits DSM's firewall,
|
||||
matches no allow rule (your database port is not in DSM's service list, and
|
||||
should not be), and is dropped before Docker's
|
||||
`-i docker-xxx -o docker-xxx -j ACCEPT` is consulted.
|
||||
and `net.bridge.bridge-nf-call-iptables=1` (the DSM default) forces traffic
|
||||
between two containers *on the same bridge* through that chain. Your database
|
||||
port is not in DSM's allow list — and should not be — so new connections are
|
||||
dropped. The `RELATED,ESTABLISHED` accept near the top is what makes it look
|
||||
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
|
||||
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
|
||||
working when the bridge is recreated under a new name. It permits traffic only
|
||||
*between* Docker bridges — nothing from the LAN or the internet gains access.
|
||||
Two containers on one bridge are a layer-2 conversation; that flag is what
|
||||
forces it through a layer-3 firewall in the first place. With it off the packets
|
||||
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
|
||||
its firewall. Add a *triggered* task in *Control Panel → Task Scheduler →
|
||||
Create → Triggered Task → User-defined script*, event **Boot-up**, user
|
||||
**root**:
|
||||
Verify — expect 10 out of 10:
|
||||
|
||||
```bash
|
||||
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
|
||||
sleep 60
|
||||
iptables -C FORWARD -i docker+ -o docker+ -j ACCEPT 2>/dev/null \
|
||||
|| iptables -I FORWARD 1 -i docker+ -o docker+ -j ACCEPT
|
||||
sysctl -w net.bridge.bridge-nf-call-iptables=0
|
||||
```
|
||||
|
||||
The `sleep` lets DSM's firewall finish loading first; `-C` checks for the rule
|
||||
so running it twice is harmless. Re-run the same script by hand after any
|
||||
change to the firewall settings.
|
||||
Re-check it after a DSM upgrade with `sysctl net.bridge.bridge-nf-call-iptables`.
|
||||
|
||||
**Verify it:**
|
||||
### Is that safe?
|
||||
|
||||
```bash
|
||||
docker run --rm --network webmail-prod postgres:16 \
|
||||
pg_isready -h postgres -p 5432 # expect: accepting connections
|
||||
```
|
||||
Yes, for this deployment, and the scope is narrower than it first sounds.
|
||||
|
||||
---
|
||||
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
|
||||
|
||||
@@ -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 |
|
||||
| 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` |
|
||||
| `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` |
|
||||
| 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` |
|
||||
|
||||
Reference in New Issue
Block a user