integration: reproduce HA primary flap to offline node via docker disconnect

Add a docker-network-disconnect primitive — `DisconnectFromNetwork`
/ `ReconnectToNetwork` on `TailscaleClient`, backed by
`pool.Client.DisconnectNetwork` — so an integration test can
faithfully simulate the cable-pull scenario the issue reporter
observed in Proxmox. Iptables-based simulations leave the socket
open at the container kernel and miss the bug.

Use it in `TestHASubnetRouterFailoverDockerDisconnect` to assert
the no-flap invariant: once primary has failed over from r1 to r2
and r2 also loses connectivity, primary must not switch back to
the still-offline r1. The assertion fails on the current branch —
the all-unhealthy fallback in `electPrimaryRoutes` flips primary
to the lowest-NodeID candidate regardless of online state. Locking
the failure in via TDD before designing the fix.

Updates #3203
This commit is contained in:
Kristoffer Dalby committed 2026-04-29 12:25:40 +00:00
1 parent 2d04fff605
commit 5228818a12
4 files changed
+250

No files matched your search

+41
View File
@@ -91,6 +91,47 @@ func AddContainerToNetwork(
return nil
}
// DisconnectContainerFromNetwork removes the container from network at
// the docker daemon level. Mirrors a physical cable pull: the
// container's network interface for that network disappears and any
// in-flight TCP connections are left half-open, exactly the failure
// mode iptables-based simulations cannot reproduce.
func DisconnectContainerFromNetwork(
pool *dockertest.Pool,
network *dockertest.Network,
testContainer string,
) error {
containers, err := pool.Client.ListContainers(docker.ListContainersOptions{
All: true,
Filters: map[string][]string{
"name": {testContainer},
},
})
if err != nil {
return err
}
if len(containers) == 0 {
return fmt.Errorf("%w: %s", ErrContainerNotFound, testContainer)
}
return pool.Client.DisconnectNetwork(network.Network.ID, docker.NetworkConnectionOptions{
Container: containers[0].ID,
Force: true,
})
}
// ReconnectContainerToNetwork is the inverse of
// DisconnectContainerFromNetwork — re-attaches the container to the
// network so traffic can flow again.
func ReconnectContainerToNetwork(
pool *dockertest.Pool,
network *dockertest.Network,
testContainer string,
) error {
return AddContainerToNetwork(pool, network, testContainer)
}
// RandomFreeHostPort asks the kernel for a free open port that is ready to use.
// (from https://github.com/phayes/freeport)
func RandomFreeHostPort() (int, error) {