diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 2110a7f..56e7419 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -72,10 +72,10 @@ print( print( "[entrypoint] If this is a Synology NAS: DSM's firewall filters traffic " "between containers (FORWARD -j FORWARD_FIREWALL runs before Docker's own " - "rules and denies by default). Fix:\n" - " iptables -I FORWARD 1 -i docker+ -o docker+ -j ACCEPT\n" - "See docs/nas-install.md, 'DSM firewall blocks container-to-container " - "traffic'.", + "rules and ends in DROP), and it reloads periodically, so an added iptables " + "rule does not survive. Fix:\n" + " sysctl -w net.bridge.bridge-nf-call-iptables=0\n" + "See docs/nas-install.md section 8.", file=sys.stderr, ) sys.exit(1) diff --git a/docs/nas-install.md b/docs/nas-install.md index 77bba9a..65515be 100644 --- a/docs/nas-install.md +++ b/docs/nas-install.md @@ -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` |