Skip to content

Make SABnzbd's bind address survive a restart, and assert the control that matters - #69

Merged
ivan-pinatti merged 6 commits into
mainfrom
fix/sabnzbd-bind-address-is-the-containers-to-choose
Aug 18, 2026
Merged

ivan-pinatti merged 6 commits into
mainfrom
fix/sabnzbd-bind-address-is-the-containers-to-choose

Conversation

@ivan-pinatti

Copy link
Copy Markdown
Collaborator

The 5.0.4 bump on #61 went red on test_sabnzbd_config_disables_ipv6: the running config read host = :: where the seeded ini says 0.0.0.0. This is the whole investigation and the fix.

SABnzbd is not at fault

DEF_HOST is 127.0.0.1 in sabnzbd/constants.py and validate_host in sabnzbd/cfg.py accepts 0.0.0.0 unchanged. The container is doing it, in root/etc/s6-overlay/s6-rc.d/svc-sabnzbd/run:

FAMILY=::
if ! test -f /proc/net/if_inet6; then FAMILY=0.0.0.0; fi
exec ... SABnzbd.py --config-file /config --server "$FAMILY"

--server overrides the config file and SABnzbd persists it back, so our value cannot survive a start. /proc/net/if_inet6 exists here because gluetun keeps IPv6 sysctls enabled for its own DNS listener, which docker-compose-vpn.yml:70 already records as deliberate.

Three changes

1. Assert the control that actually protects us. docker-compose-vpn.yml:70 says leaks are prevented by "no IPv6 addresses assigned to any interface" plus gluetun's ip6tables rules, and nothing asserted the first half. test_vpn_namespace_has_no_global_ipv6_address now reads /proc/net/if_inet6 in that namespace and fails on any scope 00 address. This is strictly more coverage than before: the old test would have passed happily while a routable IPv6 address sat on the tunnel.

2. Accept either bind address in the test. It belongs to the container, not to us, and a listener on :: reaches nothing while change 1 holds. ipv6_hosting and ipv6_servers stay asserted strictly, since --server does not touch them and they are what governs server selection and the [::1] listener.

3. Vendor the run script anyway, as belt. patches/sabnzbd/svc-sabnzbd/run narrows the condition: the family address is still imposed when the configured host is missing or loopback, so nobody can bind 127.0.0.1 and lock themselves out, which is the case upstream said it was protecting. A reachable value is respected.

Upstream

Raised as linuxserver/docker-sabnzbd#274 with the fix as #275, framed as a feature request because the bug framing is what #240 was closed under. The patch is identical to that PR, so it becomes a no-op if the change ships. docs/TODO.md carries the removal condition.

Expectations should be low. The same behavior was reported in #2 (2016), #35 (2017), #42 (2018), #116 (2021) and #240 (2024), and all five were closed, #240 as NOT_PLANNED with "That's the intended behavior".

One defect worth calling out

My first version of the patch read the host with sed -n 's/^host.*//p' | head -1. The NNTP server entries under [servers] carry their own unindented host = ... line, so on a reordered ini that returns news.example.net and hands it to --server. Fixed with section-aware extraction, verified against four ini shapes.

The 5.0.4 bump on #61 turned the suite red on
test_sabnzbd_config_disables_ipv6: the running config read `host = ::` where the
seeded ini says `0.0.0.0`.

SABnzbd is not the culprit. Its DEF_HOST is 127.0.0.1 and validate_host accepts
`0.0.0.0` unchanged. linuxserver's start script is:

    FAMILY=::
    if ! test -f /proc/net/if_inet6; then FAMILY=0.0.0.0; fi
    exec ... SABnzbd.py --config-file /config --server "$FAMILY"

`--server` overrides the config file and SABnzbd persists it back, so our value
cannot survive a start. `/proc/net/if_inet6` exists here because gluetun keeps
IPv6 sysctls enabled for its own DNS listener, which docs/HARDENING.md already
records as deliberate. Reported upstream twice, linuxserver/docker-sabnzbd#116
and #240, and closed NOT_PLANNED as intended: forcing it is what stops someone
binding 127.0.0.1 and locking themselves out of the web UI.

So the test now accepts either value for `host` and keeps `ipv6_hosting` and
`ipv6_servers` asserted strictly. Those two are untouched by `--server` and they
are what actually governs IPv6: server selection and the [::1] listener. The
bind address was never the control here anyway, since the listener sits in
gluetun's namespace, which is assigned no IPv6 address and is firewalled.

Rejected: disabling IPv6 in that namespace so the container picks 0.0.0.0, which
would break gluetun's DNS listener, and vendoring a custom start script, which
is a permanent fork of upstream's launcher to control a value that protects
nothing in this topology.
docker-compose-vpn.yml says IPv6 is left enabled in gluetun's namespace because
its DNS server binds [::]:53, and that leaks are prevented by no interface
holding an IPv6 address plus gluetun's ip6tables rules. Nothing asserted the
first half of that.

It is also the property the SABnzbd bind address was standing in for, and the
better one to test. The bind address belongs to linuxserver's start script, so
asserting it proved only what upstream felt like doing, and a listener on `::`
reaches nothing while no IPv6 address exists in the namespace. If a provider
ever hands one out inside the tunnel, this fails and the bind address becomes
worth arguing about again, which the old test would not have caught.

Read from /proc/net/if_inet6 rather than `ip -6 addr`, since the image may not
carry iproute2, and only scope 00 counts: link-local and host addresses are not
routable.
Defense in depth for the value the image overwrites. The shipped service script
passes `--server ::` on every start, `--server` beats the config file, and
SABnzbd persists the override back into sabnzbd.ini, so `host = 0.0.0.0` cannot
survive a container start. That is what turned the 5.0.4 bump red.

patches/sabnzbd/svc-sabnzbd/run replaces the script and narrows the condition:
the family address is still imposed when the configured host is missing or
loopback, so nobody can bind 127.0.0.1 and lock themselves out of the web UI,
which is the case upstream said it was protecting when it closed #240. A
reachable value is left alone. Verified against the decision table: empty,
localhost, 127.* and ::1 still get --server, while 0.0.0.0, :: and a LAN address
are respected.

This is not the control that matters here, and the patch is not load bearing.
test_vpn_namespace_has_no_global_ipv6_address asserts the property
docker-compose-vpn.yml actually relies on, no interface in gluetun's namespace
holding a routable IPv6 address, so the bind address is belt rather than braces
and the test still accepts either value. docs/TODO.md carries the condition to
check before deleting the patch.

The file is a complete GPL-3.0 file from linuxserver/docker-sabnzbd. patches/
README.md records it with its license, which is the per-directory breakdown
NOTICE.md already points at.
The first version grepped the file for `^host` and took the first match. The
NNTP server entries under [servers] carry their own unindented `host = ...`
line, so a reordered ini would have handed a news server's hostname to
--server. Demonstrated: on a file with [servers] before [misc], the old
expression returns news.example.net.

Section tracking fixes it, verified against four shapes: [misc] first, [servers]
first, a loopback value, and no host key at all.

The patch now also matches the change being proposed upstream line for line,
passing a configured host back through --server rather than dropping the flag,
so this file becomes a no-op if linuxserver takes it.
Filed linuxserver/docker-sabnzbd#274 as a feature request rather than a bug,
because the bug framing is what #240 was closed under, with the fix as #275. The
vendored patch is identical to that pull request, so it becomes a no-op if the
change ships.

The TODO now also records how often this has been raised, #2 in 2016, #35 in
2017, #42 in 2018, #116 in 2021 and #240 in 2024, all closed. Whoever reads this
next should know the odds before spending time chasing it.
@coderabbitai

coderabbitai Bot commented Aug 18, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

@ivan-pinatti, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 14 minutes

Limit details: You’ve used all 3 included reviews currently available under your plan.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3520a379-40e6-447f-abb1-ae0b9458b1bf

📥 Commits

Reviewing files that changed from the base of the PR and between 307fab8 and 76178b6.

⛔ Files ignored due to path filters (2)
  • patches/README.md is excluded by !patches/**
  • patches/sabnzbd/svc-sabnzbd/run is excluded by !patches/**
📒 Files selected for processing (4)
  • docker-compose-nzb.yml
  • docs/HARDENING.md
  • docs/TODO.md
  • tests/test_security.py

Comment @coderabbitai help to get the list of available commands.

@ivan-pinatti
ivan-pinatti marked this pull request as ready for review August 18, 2026 16:14
@ivan-pinatti

Copy link
Copy Markdown
Collaborator Author

/run-tests

@ivan-pinatti
ivan-pinatti merged commit 934deeb into main Aug 18, 2026
17 checks passed
@ivan-pinatti
ivan-pinatti deleted the fix/sabnzbd-bind-address-is-the-containers-to-choose branch August 18, 2026 18:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant