WIP: rebase fork onto upstream/main (v1.103.0) #15

Closed
codinget wants to merge 670 commits from webnet into save/webnet-2026-07-29
3 changed files with 181 additions and 9 deletions
Showing only changes of commit 732bde6e86 - Show all commits
+62
View File
@@ -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.
+92
View File
@@ -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
+27 -9
View File
@@ -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))
}