Files
headscale/integration/tailscale.go
T
Kristoffer Dalby 5228818a12 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
2026-04-29 12:25:40 +00:00

70 lines
2.2 KiB
Go

package integration
import (
"io"
"net/netip"
"net/url"
"time"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/juanfont/headscale/integration/dockertestutil"
"github.com/juanfont/headscale/integration/tsic"
"github.com/ory/dockertest/v3"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/netcheck"
"tailscale.com/types/key"
"tailscale.com/types/netmap"
"tailscale.com/wgengine/filter"
)
// nolint
type TailscaleClient interface {
Hostname() string
Shutdown() (string, string, error)
Version() string
Execute(
command []string,
options ...dockertestutil.ExecuteCommandOption,
) (string, string, error)
Login(loginServer, authKey string) error
LoginWithURL(loginServer string) (*url.URL, error)
Logout() error
Restart() error
Up() error
Down() error
IPs() ([]netip.Addr, error)
MustIPs() []netip.Addr
IPv4() (netip.Addr, error)
MustIPv4() netip.Addr
MustIPv6() netip.Addr
FQDN() (string, error)
MustFQDN() string
Status(...bool) (*ipnstate.Status, error)
MustStatus() *ipnstate.Status
Netmap() (*netmap.NetworkMap, error)
DebugDERPRegion(region string) (*ipnstate.DebugDERPRegionReport, error)
GetNodePrivateKey() (*key.NodePrivate, error)
Netcheck() (*netcheck.Report, error)
WaitForNeedsLogin(timeout time.Duration) error
WaitForRunning(timeout time.Duration) error
WaitForPeers(expected int, timeout, retryInterval time.Duration) error
Ping(hostnameOrIP string, opts ...tsic.PingOption) error
Curl(url string, opts ...tsic.CurlOption) (string, error)
CurlFailFast(url string) (string, error)
Traceroute(netip.Addr) (util.Traceroute, error)
ContainerID() string
MustID() types.NodeID
ReadFile(path string) ([]byte, error)
PacketFilter() ([]filter.Match, error)
ConnectToNetwork(network *dockertest.Network) error
DisconnectFromNetwork(network *dockertest.Network) error
ReconnectToNetwork(network *dockertest.Network) error
// FailingPeersAsString returns a formatted-ish multi-line-string of peers in the client
// and a bool indicating if the clients online count and peer count is equal.
FailingPeersAsString() (string, bool, error)
WriteLogs(stdout, stderr io.Writer) error
}