docs(ipv6): pin the pre-responder ndppd upgrade path
A host can sit on a pre-1.6 release (distro ndppd, old oneshot vps-ipv6.service) for a long time before upgrading, so document and lock down how install.sh converges it: - the old ndppd/npd6 daemons are disabled and stopped, the unit is replaced by the in-tree responder, and the legacy per-address kernel proxy_ndp entries are swept on ipv6-reapply; - /etc/vpsmgr/ndppd.conf is read AS-IS, so the nested `iface <bridge>` block and its indentation written by the old panel must keep parsing. Adds TestLoadRulesAcceptsPreResponderFile to a regression guard, since every existing container would lose inbound IPv6 if that format ever diverged; - the old sudoers grants disappear with the rewritten whitelist, and the distro ndppd package is left installed but inert. Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
This commit is contained in:
1 parent
4c93ae4be3
commit
8d2d31041f
2 files changed
+69
No files matched your search
@@ -152,6 +152,31 @@ route. So the new code **relies on** the isolation layer's behaviour and
|
||||
coexists with it — one closes the neighbour-spoofing hole, the other makes
|
||||
strict upstreams able to reach the block through the existing design.
|
||||
|
||||
## Upgrading a host that still runs the distro ndppd
|
||||
|
||||
An operator can sit on an old (pre-responder) release for a long time, so
|
||||
`install.sh` has to converge such a host in one run. Everything below is
|
||||
idempotent and needs no manual step beyond re-running the installer:
|
||||
|
||||
| Left behind by the old release | What the upgrade does |
|
||||
|---|---|
|
||||
| `ndppd.service` enabled and running | `vps install` disables and stops it (`systemctl disable --now ndppd.service`), and `npd6.service` with it — either would emit competing link-local-source NAs |
|
||||
| `vps-ipv6.service` = the old oneshot unit | stopped, then rewritten as the responder unit (`ExecStart=/usr/local/bin/vps ipv6-proxy`, `ExecStartPre=… ipv6-reapply`) and re-enabled |
|
||||
| `/etc/vpsmgr/ndppd.conf` in the old `rule <cidr> { iface <bridge> }` form | read as-is. The filename and format were deliberately kept; `loadRules` takes the CIDR from the `rule` line and ignores the nested block, so no regeneration is needed (`TestLoadRulesAcceptsPreResponderFile`) |
|
||||
| `/etc/ndppd.conf` symlink → `/etc/vpsmgr/ndppd.conf` | harmless and left alone: the responder opens the real `/etc/vpsmgr/ndppd.conf` directly |
|
||||
| Per-address kernel `proxy_ndp` entries and `/128` routes from the older scheme | swept on `ipv6-reapply` by `cleanLegacyKernelProxy` (limited to addresses inside the configured prefix) |
|
||||
| Old sudoers grants (`service ndppd restart`, `pkill -x ndppd`, `ln -sf /etc/ndppd.conf`, …) | the whitelist is rewritten from scratch, so those grants disappear. The old panel never needs them again — the binary is replaced in the same run, and an old binary that somehow ran would simply fail on them |
|
||||
| The distro `ndppd` package (installed by the old `install.sh`) | left installed but inactive — it is harmless while disabled, and leaving it keeps the upgrade offline-friendly. `uninstall.sh` stops and disables its service and removes its config files, but does not purge the package |
|
||||
|
||||
The rule file being read rather than regenerated is the one place where this
|
||||
upgrade is load-bearing: if the format ever diverged, every existing container
|
||||
would lose inbound IPv6 the moment the responder took over. Keep the parser
|
||||
tolerant of the legacy layout.
|
||||
|
||||
Note that this is unlike the Traefik → HAProxy migration, where the old
|
||||
per-domain files ARE discarded: those are a derived artifact of the database,
|
||||
while `ndppd.conf` is the live input of a running process.
|
||||
|
||||
## Notes on a few deliberate decisions
|
||||
|
||||
- **File name kept as `ndppd.conf`.** The format (bare `rule <cidr> {}`) is
|
||||
|
||||
@@ -58,6 +58,50 @@ func TestFilterRules(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadRulesAcceptsPreResponderFile pins the UPGRADE path from a pre-1.6
|
||||
// (Traefik-era, distro-ndppd) install: that panel rendered each rule with the
|
||||
// bridge as a nested `iface` argument and three-space indentation —
|
||||
//
|
||||
// rule 2001:db8:1::/112 {
|
||||
// iface incusbr0
|
||||
// }
|
||||
//
|
||||
// The distro daemon is gone now, but the FILE is not regenerated during an
|
||||
// upgrade, so the in-tree responder has to read exactly this on the first boot
|
||||
// after `install.sh`. If the rule indentation or the nested block ever became
|
||||
// significant, every existing container would lose inbound IPv6 the moment the
|
||||
// new responder took over.
|
||||
func TestLoadRulesAcceptsPreResponderFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "ndppd.conf")
|
||||
legacy := "proxy eth0 {\n" +
|
||||
" rule 2001:db8:1::/112 {\n" +
|
||||
" iface incusbr0\n" +
|
||||
" }\n" +
|
||||
" rule 2001:db8:2::/112 {\n" +
|
||||
" iface incusbr0\n" +
|
||||
" }\n" +
|
||||
"}\n"
|
||||
if err := os.WriteFile(path, []byte(legacy), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rules, err := loadRules(path)
|
||||
if err != nil {
|
||||
t.Fatalf("legacy ndppd file rejected: %v", err)
|
||||
}
|
||||
if len(rules) != 2 {
|
||||
t.Fatalf("got %d rules, want 2", len(rules))
|
||||
}
|
||||
for _, addr := range []string{"2001:db8:1::1", "2001:db8:1::9999", "2001:db8:2::5"} {
|
||||
if !matches(net.ParseIP(addr), rules) {
|
||||
t.Errorf("%s did not match any legacy rule", addr)
|
||||
}
|
||||
}
|
||||
if matches(net.ParseIP("2001:db8:3::1"), rules) {
|
||||
t.Error("address outside every legacy rule matched")
|
||||
}
|
||||
}
|
||||
|
||||
func mustCIDR(t *testing.T, s string) *net.IPNet {
|
||||
t.Helper()
|
||||
_, n, err := net.ParseCIDR(s)
|
||||
|
||||
Reference in new issue
Block a user