WIP: rebase fork onto upstream/main (v1.103.0) #15
@@ -1166,6 +1166,68 @@ func (e *Env) RotateDiscoKey(n *Node) {
|
||||
}
|
||||
}
|
||||
|
||||
// ForcePreferredDERP pins n's home DERP to the given region via the
|
||||
// "force-prefer-derp" debug action, so its reported NetInfo.PreferredDERP is
|
||||
// deterministic. The force lives on the long-lived magicsock.Conn and so
|
||||
// persists across an in-process profile switch. It fatals the test on error.
|
||||
func (e *Env) ForcePreferredDERP(n *Node, region int) {
|
||||
e.t.Helper()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
b, err := json.Marshal(region)
|
||||
if err != nil {
|
||||
e.t.Fatalf("ForcePreferredDERP(%s): %v", n.name, err)
|
||||
}
|
||||
if err := n.agent.DebugActionBody(ctx, "force-prefer-derp", bytes.NewReader(b)); err != nil {
|
||||
e.t.Fatalf("ForcePreferredDERP(%s, %d): %v", n.name, region, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Relogin switches n to a fresh login profile on the same test control server,
|
||||
// in-process (no daemon restart), so it comes up under a NEW node identity while
|
||||
// keeping the same long-lived magicsock.Conn. This is the control-client swap
|
||||
// that an interactive login or profile switch performs, and is what the
|
||||
// home-DERP re-report fix guards (see [magicsock.Conn.ResetNetInfoLast]).
|
||||
//
|
||||
// It switches to an empty profile (the in-process control-client swap the
|
||||
// LocalAPI PUT /profiles/ performs) and then logs back in with "tailscale up",
|
||||
// which both points the new control client at the test control and drives
|
||||
// registration to completion. It waits for the node to return to Running and
|
||||
// fatals the test on error.
|
||||
func (e *Env) Relogin(n *Node) {
|
||||
e.t.Helper()
|
||||
// Generous timeout: the profile switch triggers a fresh registration +
|
||||
// netcheck + DERP connect, which is slow under TCG (no KVM).
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// Switch to a fresh, empty login profile. This runs the in-process control-
|
||||
// client swap (resetForProfileChangeLocked -> setControlClientLocked) that
|
||||
// clears the home-DERP dedup cache under test, while preserving the existing
|
||||
// magicsock.Conn (and any forced home DERP from [Env.ForcePreferredDERP]).
|
||||
if err := n.agent.SwitchToEmptyProfile(ctx); err != nil {
|
||||
e.t.Fatalf("Relogin(%s): SwitchToEmptyProfile: %v", n.name, err)
|
||||
}
|
||||
// Log back in to the same test control. "tailscale up --login-server" points
|
||||
// the new control client at the test control and drives registration to
|
||||
// completion (testcontrol auto-authorizes), the same path Env.Start uses.
|
||||
if err := e.tailscaleUp(ctx, n); err != nil {
|
||||
e.t.Fatalf("Relogin(%s): up: %v", n.name, err)
|
||||
}
|
||||
if err := tstest.WaitFor(60*time.Second, func() error {
|
||||
st, err := n.agent.Status(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if st.BackendState != "Running" {
|
||||
return fmt.Errorf("backend state = %q, want Running", st.BackendState)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
e.t.Fatalf("Relogin(%s): %v", n.name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// RestartTailscaled signals tailscaled on n to die so that its supervisor
|
||||
// (gokrazy) restarts it. It then waits for tailscaled to come back to the
|
||||
// "Running" backend state. It fatals the test on error.
|
||||
|
||||
@@ -820,6 +820,98 @@ func checkDiscoRotated(t *testing.T, env *vmtest.Env, a, b, pingFrom, pingTo *vm
|
||||
return newDisco
|
||||
}
|
||||
|
||||
// TestHomeDERPReportedAfterRelogin is a regression test for the bug where, after
|
||||
// an in-process control-client swap (an interactive login or profile switch),
|
||||
// magicsock's NetInfo de-dup cache (netInfoLast) survived the swap. Because the
|
||||
// post-relogin NetInfo was structurally identical (same PreferredDERP, same NAT
|
||||
// shape), it was suppressed as unchanged and never re-reported to the new
|
||||
// control session, so control never learned the node's home DERP and peers
|
||||
// couldn't reach it over DERP. See ipn/ipnlocal:
|
||||
// setControlClientLocked -> MagicConn().ResetNetInfoLast.
|
||||
//
|
||||
// The test brings a node up, records the home DERP region the test control
|
||||
// learned, re-logs the node in (new node identity, same control/network/NAT/
|
||||
// DERP), and asserts control re-learns the same non-zero home DERP for the new
|
||||
// identity. Without the fix this assertion times out at HomeDERP==0.
|
||||
func TestHomeDERPReportedAfterRelogin(t *testing.T) {
|
||||
env := vmtest.New(t)
|
||||
net := env.AddNetwork("2.1.1.1", "192.168.1.1/24", vnet.EasyNAT)
|
||||
n := env.AddNode("node", net, vmtest.OS(vmtest.Gokrazy))
|
||||
|
||||
baseStep := env.AddStep("Record initial home DERP from control")
|
||||
switchStep := env.AddStep("Re-login (logout + up)")
|
||||
verifyStep := env.AddStep("Verify home DERP re-reported to control after relogin")
|
||||
|
||||
env.Start()
|
||||
|
||||
cs := env.ControlServer()
|
||||
|
||||
// Pin the home DERP region so the reported NetInfo (including PreferredDERP)
|
||||
// is identical before and after the switch. natlab has two DERP regions with
|
||||
// no latency differentiation, so the natural pick could differ across the
|
||||
// switch; a changed PreferredDERP would NOT be de-duped and would mask the
|
||||
// regression. The force persists on the long-lived magicsock.Conn across the
|
||||
// in-process profile switch.
|
||||
const region = 1
|
||||
env.ForcePreferredDERP(n, region)
|
||||
|
||||
// Baseline: control learned the (forced) home DERP for the initial identity.
|
||||
baseStep.Begin()
|
||||
st := env.Status(n)
|
||||
oldKey := st.Self.PublicKey
|
||||
if err := tstest.WaitFor(30*time.Second, func() error {
|
||||
cn := cs.Node(oldKey)
|
||||
if cn == nil {
|
||||
return fmt.Errorf("control has no node for initial key %v", oldKey.ShortString())
|
||||
}
|
||||
if cn.HomeDERP != region {
|
||||
return fmt.Errorf("control home DERP for initial identity = %d, want %d", cn.HomeDERP, region)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
baseStep.Fatal(err)
|
||||
}
|
||||
t.Logf("[node] initial: key=%s homeDERP=%d", oldKey.ShortString(), region)
|
||||
baseStep.End(nil)
|
||||
|
||||
// Re-login on the same control/network/NAT/DERP. The new identity reports a
|
||||
// structurally-identical NetInfo, which the buggy de-dup would have
|
||||
// suppressed. (logout + up funnels through the same
|
||||
// resetForProfileChangeLocked -> setControlClientLocked path as a
|
||||
// localapi PUT /profiles/ switch.)
|
||||
switchStep.Begin()
|
||||
env.Relogin(n)
|
||||
st2 := env.Status(n)
|
||||
newKey := st2.Self.PublicKey
|
||||
if newKey == oldKey {
|
||||
switchStep.Fatalf("node key unchanged after relogin: %v", newKey.ShortString())
|
||||
}
|
||||
t.Logf("[node] after relogin: key=%s", newKey.ShortString())
|
||||
switchStep.End(nil)
|
||||
|
||||
// Regression assertion: control must re-learn the same non-zero home DERP for
|
||||
// the new identity. Times out at HomeDERP==0 without ResetNetInfoLast.
|
||||
verifyStep.Begin()
|
||||
if err := tstest.WaitFor(60*time.Second, func() error {
|
||||
cn := cs.Node(newKey)
|
||||
if cn == nil {
|
||||
return fmt.Errorf("control has no node for new key %v yet", newKey.ShortString())
|
||||
}
|
||||
if cn.HomeDERP == 0 {
|
||||
return fmt.Errorf("home DERP not re-reported after profile switch (HomeDERP=0)")
|
||||
}
|
||||
if cn.HomeDERP != region {
|
||||
return fmt.Errorf("home DERP region changed across switch: was %d, now %d", region, cn.HomeDERP)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
env.DumpStatus(n)
|
||||
verifyStep.Fatal(err)
|
||||
}
|
||||
t.Logf("[node] home DERP %d re-reported to control after profile switch", region)
|
||||
verifyStep.End(nil)
|
||||
}
|
||||
|
||||
// TestMullvadExitNode verifies that a Tailscale client whose netmap contains
|
||||
// a plain-WireGuard exit node (the way Mullvad exit nodes are wired up by
|
||||
// the control plane) can route internet traffic through it, with the source
|
||||
|
||||
@@ -374,13 +374,22 @@ func (n *network) acceptTCP(r *tcp.ForwarderRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
if destPort == 80 && fakeControl.Match(destIP) {
|
||||
if fakeControl.Match(destIP) && (destPort == 80 || destPort == 443) {
|
||||
r.Complete(false)
|
||||
tc := gonet.NewTCPConn(&wq, ep)
|
||||
context.AfterFunc(n.s.shutdownCtx, func() { tc.SetDeadline(time.Now()) })
|
||||
// The control client's noise dialer forces an HTTPS (port 443) dial when
|
||||
// it made a noise dial recently — e.g. an immediate re-login or profile
|
||||
// switch; see controlhttp.Dialer.forceNoise443. Serve the test control
|
||||
// over TLS on 443 too so that path reaches it. (The cert isn't
|
||||
// validated: noise dials authenticate via the Noise handshake.)
|
||||
var ln net.Listener = netutil.NewOneConnListener(tc, nil)
|
||||
if destPort == 443 {
|
||||
ln = netutil.NewOneConnListener(tls.Server(tc, n.s.controlTLS), nil)
|
||||
}
|
||||
hs := &http.Server{Handler: n.s.control}
|
||||
n.s.wg.Go(func() {
|
||||
hs.Serve(netutil.NewOneConnListener(tc, nil))
|
||||
hs.Serve(ln)
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -776,7 +785,7 @@ type derpServer struct {
|
||||
// want to exercise sha256-raw cert pinning can read the certSHA256Hex via
|
||||
// [Server.DERPCertSHA256Hex].
|
||||
func newDERPServer(hostname string) *derpServer {
|
||||
tlsConfig, certHex := selfSignedDERPCert(hostname)
|
||||
tlsConfig, certHex := selfSignedCert(hostname)
|
||||
ds := &derpServer{
|
||||
srv: derpserver.New(key.NewNode(), logger.Discard),
|
||||
tlsConfig: tlsConfig,
|
||||
@@ -790,10 +799,10 @@ func newDERPServer(hostname string) *derpServer {
|
||||
return ds
|
||||
}
|
||||
|
||||
// selfSignedDERPCert builds a self-signed ECDSA P-256 cert valid for hostname
|
||||
// and returns a *tls.Config that serves it, along with the SHA-256 hex digest
|
||||
// of the cert's DER bytes.
|
||||
func selfSignedDERPCert(hostname string) (*tls.Config, string) {
|
||||
// selfSignedCert builds a self-signed ECDSA P-256 cert valid for hostname and
|
||||
// returns a *tls.Config that serves it, along with the SHA-256 hex digest of
|
||||
// the cert's DER bytes (used by DERP for sha256-raw cert pinning).
|
||||
func selfSignedCert(hostname string) (*tls.Config, string) {
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), crand.Reader)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("vnet: generating DERP cert key: %v", err))
|
||||
@@ -840,7 +849,12 @@ type Server struct {
|
||||
networks set.Set[*network]
|
||||
networkByWAN *bart.Table[*network]
|
||||
|
||||
control *testcontrol.Server
|
||||
control *testcontrol.Server
|
||||
// controlTLS is a self-signed cert for serving the test control over HTTPS
|
||||
// (port 443) in addition to plaintext HTTP. The control client does not
|
||||
// validate this cert (noise dials authenticate via the Noise handshake, not
|
||||
// the outer TLS); it exists only so the forced-443 dial path has a TLS peer.
|
||||
controlTLS *tls.Config
|
||||
derps []*derpServer
|
||||
fakeACME *fakeACMEServer
|
||||
pcapWriter *pcapWriter
|
||||
@@ -888,6 +902,9 @@ func (s *Server) SetDHCPCallback(fn func(MAC, int, layers.DHCPMsgType, netip.Add
|
||||
// hostname verification succeeds for tests that pin via sha256-raw.
|
||||
var derpHostnames = []string{"derp1.tailscale", "derp2.tailscale"}
|
||||
|
||||
// controlHostname is the hostname the fake control server is reached at.
|
||||
const controlHostname = "control.tailscale"
|
||||
|
||||
var derpMap = &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
1: {
|
||||
@@ -933,7 +950,7 @@ func New(c *Config) (*Server, error) {
|
||||
|
||||
control: &testcontrol.Server{
|
||||
DERPMap: derpMap,
|
||||
ExplicitBaseURL: "http://control.tailscale",
|
||||
ExplicitBaseURL: "http://" + controlHostname,
|
||||
},
|
||||
fakeACME: newFakeACMEServer("http://acme.example"),
|
||||
|
||||
@@ -950,6 +967,7 @@ func New(c *Config) (*Server, error) {
|
||||
return nil
|
||||
}
|
||||
s.fakeACME.lookupTXT = s.lookupTXT
|
||||
s.controlTLS, _ = selfSignedCert(controlHostname)
|
||||
for _, host := range derpHostnames {
|
||||
s.derps = append(s.derps, newDERPServer(host))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user