diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go index 68ab9ca17..70ebe2f23 100644 --- a/control/controlclient/direct.go +++ b/control/controlclient/direct.go @@ -1086,7 +1086,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap } else { vlogf("netmap: got new map") } - if resp.ControlDialPlan != nil { + if resp.ControlDialPlan != nil && !ignoreDialPlan() { if c.dialPlan != nil { c.logf("netmap: got new dial plan from control") c.dialPlan.Store(resp.ControlDialPlan) @@ -1774,6 +1774,13 @@ func makeScreenTimeDetectingDialFunc(dial dialFunc) (dialFunc, *atomic.Bool) { }, ab } +func ignoreDialPlan() bool { + // If we're running in v86 (a JavaScript-based emulation of a 32-bit x86) + // our networking is very limited. Let's ignore the dial plan since it's too + // complicated to race that many IPs anyway. + return hostinfo.IsInVM86() +} + func isTCPLoopback(a net.Addr) bool { if ta, ok := a.(*net.TCPAddr); ok { return ta.IP.IsLoopback() diff --git a/control/controlclient/map.go b/control/controlclient/map.go index 769c8f1e3..3173040fe 100644 --- a/control/controlclient/map.go +++ b/control/controlclient/map.go @@ -19,6 +19,7 @@ import ( "tailscale.com/control/controlknobs" "tailscale.com/envknob" + "tailscale.com/hostinfo" "tailscale.com/tailcfg" "tailscale.com/tstime" "tailscale.com/types/key" @@ -308,6 +309,31 @@ func (ms *mapSession) updateStateFromResponse(resp *tailcfg.MapResponse) { } } + // In the copy/v86 wasm environment with limited networking, if the + // control plane didn't pick our DERP home for us, do it ourselves and + // mark all but the lowest region as NoMeasureNoHome. For prod, this + // will be Region 1, NYC, a compromise between the US and Europe. But + // really the control plane should pick this. This is only a fallback. + if hostinfo.IsInVM86() { + numCanMeasure := 0 + lowest := 0 + for rid, r := range dm.Regions { + if !r.NoMeasureNoHome { + numCanMeasure++ + if lowest == 0 || rid < lowest { + lowest = rid + } + } + } + if numCanMeasure > 1 { + for rid, r := range dm.Regions { + if rid != lowest { + r.NoMeasureNoHome = true + } + } + } + } + // Zero-valued fields in a DERPMap mean that we're not changing // anything and are using the previous value(s). if ldm := ms.lastDERPMap; ldm != nil { diff --git a/hostinfo/hostinfo.go b/hostinfo/hostinfo.go index d952ce603..afb465ece 100644 --- a/hostinfo/hostinfo.go +++ b/hostinfo/hostinfo.go @@ -21,6 +21,7 @@ import ( "go4.org/mem" "tailscale.com/envknob" "tailscale.com/tailcfg" + "tailscale.com/types/lazy" "tailscale.com/types/opt" "tailscale.com/types/ptr" "tailscale.com/util/cloudenv" @@ -497,5 +498,14 @@ func IsNATLabGuestVM() bool { return false } -// NAT Lab VMs have a unique MAC address prefix. -// See +const copyV86DeviceModel = "copy-v86" + +var isV86Cache lazy.SyncValue[bool] + +// IsInVM86 reports whether we're running in the copy/v86 wasm emulator, +// https://github.com/copy/v86/. +func IsInVM86() bool { + return isV86Cache.Get(func() bool { + return New().DeviceModel == copyV86DeviceModel + }) +} diff --git a/hostinfo/hostinfo_plan9.go b/hostinfo/hostinfo_plan9.go new file mode 100644 index 000000000..f9aa30e51 --- /dev/null +++ b/hostinfo/hostinfo_plan9.go @@ -0,0 +1,39 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package hostinfo + +import ( + "bytes" + "os" + "strings" + + "tailscale.com/tailcfg" + "tailscale.com/types/lazy" +) + +func init() { + RegisterHostinfoNewHook(func(hi *tailcfg.Hostinfo) { + if isPlan9V86() { + hi.DeviceModel = copyV86DeviceModel + } + }) +} + +var isPlan9V86Cache lazy.SyncValue[bool] + +// isPlan9V86 reports whether we're running in the wasm +// environment (https://github.com/copy/v86/). +func isPlan9V86() bool { + return isPlan9V86Cache.Get(func() bool { + v, _ := os.ReadFile("/dev/cputype") + s, _, _ := strings.Cut(string(v), " ") + if s != "PentiumIV/Xeon" { + return false + } + + v, _ = os.ReadFile("/dev/config") + v, _, _ = bytes.Cut(v, []byte{'\n'}) + return string(v) == "# pcvm - small kernel used to run in vm" + }) +} diff --git a/net/netcheck/netcheck.go b/net/netcheck/netcheck.go index 5f4ab41c2..c9f03966b 100644 --- a/net/netcheck/netcheck.go +++ b/net/netcheck/netcheck.go @@ -25,6 +25,7 @@ import ( "tailscale.com/derp/derphttp" "tailscale.com/envknob" + "tailscale.com/hostinfo" "tailscale.com/net/captivedetection" "tailscale.com/net/dnscache" "tailscale.com/net/neterror" @@ -863,7 +864,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap, opts *GetRe c.curState = nil }() - if runtime.GOOS == "js" || runtime.GOOS == "tamago" { + if runtime.GOOS == "js" || runtime.GOOS == "tamago" || (runtime.GOOS == "plan9" && hostinfo.IsInVM86()) { if err := c.runHTTPOnlyChecks(ctx, last, rs, dm); err != nil { return nil, err } @@ -1063,6 +1064,19 @@ func (c *Client) runHTTPOnlyChecks(ctx context.Context, last *Report, rs *report regions = append(regions, dr) } } + + if len(regions) == 1 && hostinfo.IsInVM86() { + // If we only have 1 region that's probably and we're in a + // network-limited v86 environment, don't actually probe it. Just fake + // some results. + rg := regions[0] + if len(rg.Nodes) > 0 { + node := rg.Nodes[0] + rs.addNodeLatency(node, netip.AddrPort{}, 999*time.Millisecond) + return nil + } + } + c.logf("running HTTP-only netcheck against %v regions", len(regions)) var wg sync.WaitGroup diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index 313f9e315..a32867f72 100644 --- a/wgengine/magicsock/magicsock.go +++ b/wgengine/magicsock/magicsock.go @@ -719,7 +719,7 @@ func (c *Conn) updateEndpoints(why string) { c.muCond.Broadcast() }() c.dlogf("[v1] magicsock: starting endpoint update (%s)", why) - if c.noV4Send.Load() && runtime.GOOS != "js" && !c.onlyTCP443.Load() { + if c.noV4Send.Load() && runtime.GOOS != "js" && !c.onlyTCP443.Load() && !hostinfo.IsInVM86() { c.mu.Lock() closed := c.closed c.mu.Unlock() @@ -2767,7 +2767,9 @@ func (c *Conn) Rebind() { c.logf("Rebind; defIf=%q, ips=%v", defIf, ifIPs) } - c.maybeCloseDERPsOnRebind(ifIPs) + if len(ifIPs) > 0 { + c.maybeCloseDERPsOnRebind(ifIPs) + } c.resetEndpointStates() }