diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index 4917740e0..48d40fc78 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -8,7 +8,6 @@ import ( "bytes" "encoding/json" "errors" - "fmt" "io" "net" "net/http" @@ -22,8 +21,6 @@ import ( "time" "tailscale.com/client/tailscale/apitype" - "tailscale.com/envknob" - "tailscale.com/hostinfo" "tailscale.com/ipn" "tailscale.com/ipn/ipnauth" "tailscale.com/ipn/ipnlocal" @@ -36,8 +33,6 @@ import ( "tailscale.com/types/ptr" "tailscale.com/util/clientmetric" "tailscale.com/util/httpm" - "tailscale.com/util/mak" - "tailscale.com/util/rands" "tailscale.com/version" ) @@ -52,25 +47,20 @@ var handler = map[string]localAPIHandler{ // The other /localapi/v0/NAME handlers are exact matches and contain only NAME // without a trailing slash: - "alpha-set-device-attrs": (*Handler).serveSetDeviceAttrs, // see tailscale/corp#24690 - "bugreport": (*Handler).serveBugReport, - "check-ip-forwarding": (*Handler).serveCheckIPForwarding, - "check-prefs": (*Handler).serveCheckPrefs, - "disconnect-control": (*Handler).disconnectControl, - "goroutines": (*Handler).serveGoroutines, - "id-token": (*Handler).serveIDToken, - "login-interactive": (*Handler).serveLoginInteractive, - "logout": (*Handler).serveLogout, - "metrics": (*Handler).serveMetrics, - "prefs": (*Handler).servePrefs, - "query-feature": (*Handler).serveQueryFeature, - "reload-config": (*Handler).reloadConfig, - "reset-auth": (*Handler).serveResetAuth, - "start": (*Handler).serveStart, - "status": (*Handler).serveStatus, - "suggest-exit-node": (*Handler).serveSuggestExitNode, - "watch-ipn-bus": (*Handler).serveWatchIPNBus, - "whois": (*Handler).serveWhoIs, + "check-prefs": (*Handler).serveCheckPrefs, + "disconnect-control": (*Handler).disconnectControl, + "goroutines": (*Handler).serveGoroutines, + "login-interactive": (*Handler).serveLoginInteractive, + "logout": (*Handler).serveLogout, + "metrics": (*Handler).serveMetrics, + "prefs": (*Handler).servePrefs, + "query-feature": (*Handler).serveQueryFeature, + "reload-config": (*Handler).reloadConfig, + "reset-auth": (*Handler).serveResetAuth, + "start": (*Handler).serveStart, + "status": (*Handler).serveStatus, + "watch-ipn-bus": (*Handler).serveWatchIPNBus, + "whois": (*Handler).serveWhoIs, } var ( @@ -209,183 +199,10 @@ func (*Handler) serveLocalAPIRoot(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "tailscaled\n") } -// serveIDToken handles requests to get an OIDC ID token. -func (h *Handler) serveIDToken(w http.ResponseWriter, r *http.Request) { - if !h.PermitWrite { - http.Error(w, "id-token access denied", http.StatusForbidden) - return - } - nm := h.b.NetMap() - if nm == nil { - http.Error(w, "no netmap", http.StatusServiceUnavailable) - return - } - aud := strings.TrimSpace(r.FormValue("aud")) - if len(aud) == 0 { - http.Error(w, "no audience requested", http.StatusBadRequest) - return - } - req := &tailcfg.TokenRequest{ - CapVersion: tailcfg.CurrentCapabilityVersion, - Audience: aud, - NodeKey: nm.NodeKey, - } - b, err := json.Marshal(req) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - httpReq, err := http.NewRequest("POST", "https://unused/machine/id-token", bytes.NewReader(b)) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - resp, err := h.b.DoNoiseRequest(httpReq) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - defer resp.Body.Close() - w.WriteHeader(resp.StatusCode) - if _, err := io.Copy(w, resp.Body); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } -} - -func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) { - if !h.PermitRead { - http.Error(w, "bugreport access denied", http.StatusForbidden) - return - } - if r.Method != "POST" { - http.Error(w, "only POST allowed", http.StatusMethodNotAllowed) - return - } - - logMarker := func() string { - return fmt.Sprintf("BUG-%v-%v-%v", h.backendLogID, h.clock.Now().UTC().Format("20060102150405Z"), rands.HexString(16)) - } - if envknob.NoLogsNoSupport() { - logMarker = func() string { return "BUG-NO-LOGS-NO-SUPPORT-this-node-has-had-its-logging-disabled" } - } - - startMarker := logMarker() - h.logf("user bugreport: %s", startMarker) - if note := r.URL.Query().Get("note"); len(note) > 0 { - h.logf("user bugreport note: %s", note) - } - hi, _ := json.Marshal(hostinfo.New()) - h.logf("user bugreport hostinfo: %s", hi) - if err := h.b.HealthTracker().OverallError(); err != nil { - h.logf("user bugreport health: %s", err.Error()) - } else { - h.logf("user bugreport health: ok") - } - - // Information about the current node from the netmap - if nm := h.b.NetMap(); nm != nil { - if self := nm.SelfNode; self.Valid() { - h.logf("user bugreport node info: nodeid=%q stableid=%q expiry=%q", self.ID(), self.StableID(), self.KeyExpiry().Format(time.RFC3339)) - } - h.logf("user bugreport public keys: machine=%q node=%q", nm.MachineKey, nm.NodeKey) - } else { - h.logf("user bugreport netmap: no active netmap") - } - - // Print all envknobs; we otherwise only print these on startup, and - // printing them here ensures we don't have to go spelunking through - // logs for them. - envknob.LogCurrent(logger.WithPrefix(h.logf, "user bugreport: ")) - - if defBool(r.URL.Query().Get("diagnose"), false) { - } - w.Header().Set("Content-Type", "text/plain") - fmt.Fprintln(w, startMarker) - - // Nothing else to do if we're not in record mode; we wrote the marker - // above, so we can just finish our response now. - if !defBool(r.URL.Query().Get("record"), false) { - return - } - - until := h.clock.Now().Add(12 * time.Hour) - - var changed map[string]bool - for _, component := range []string{"magicsock"} { - if h.b.GetComponentDebugLogging(component).IsZero() { - if err := h.b.SetComponentDebugLogging(component, until); err != nil { - h.logf("bugreport: error setting component %q logging: %v", component, err) - continue - } - - mak.Set(&changed, component, true) - } - } - defer func() { - for component := range changed { - h.b.SetComponentDebugLogging(component, time.Time{}) - } - }() - - // NOTE(andrew): if we have anything else we want to do while recording - // a bugreport, we can add it here. - - // Read from the client; this will also return when the client closes - // the connection. - var buf [1]byte - _, err := r.Body.Read(buf[:]) - - switch { - case err == nil: - // good - case errors.Is(err, io.EOF): - // good - case errors.Is(err, io.ErrUnexpectedEOF): - // this happens when Ctrl-C'ing the tailscale client; don't - // bother logging an error - default: - // Log but continue anyway. - h.logf("user bugreport: error reading body: %v", err) - } - - // Generate another log marker and return it to the client. - endMarker := logMarker() - h.logf("user bugreport end: %s", endMarker) - fmt.Fprintln(w, endMarker) -} - func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) { h.serveWhoIsWithBackend(w, r, h.b) } -// serveSetDeviceAttrs is (as of 2024-12-30) an experimental LocalAPI handler to -// set device attributes via the control plane. -// -// See tailscale/corp#24690. -func (h *Handler) serveSetDeviceAttrs(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - if !h.PermitWrite { - http.Error(w, "set-device-attrs access denied", http.StatusForbidden) - return - } - if r.Method != "PATCH" { - http.Error(w, "only PATCH allowed", http.StatusMethodNotAllowed) - return - } - var req map[string]any - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - if err := h.b.SetDeviceAttrs(ctx, req); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - w.Header().Set("Content-Type", "application/json") - io.WriteString(w, "{}\n") -} - // localBackendWhoIsMethods is the subset of ipn.LocalBackend as needed // by the localapi WhoIs method. type localBackendWhoIsMethods interface { @@ -560,23 +377,6 @@ func authorizeServeConfigForGOOSAndUserContext(goos string, configIn *ipn.ServeC } -func (h *Handler) serveCheckIPForwarding(w http.ResponseWriter, r *http.Request) { - if !h.PermitRead { - http.Error(w, "IP forwarding check access denied", http.StatusForbidden) - return - } - var warning string - if err := h.b.CheckIPForwarding(); err != nil { - warning = err.Error() - } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(struct { - Warning string - }{ - Warning: warning, - }) -} - func (h *Handler) serveStatus(w http.ResponseWriter, r *http.Request) { if !h.PermitRead { http.Error(w, "status access denied", http.StatusForbidden) @@ -997,18 +797,3 @@ var ( metricDebugMetricsCalls = clientmetric.NewCounter("localapi_debugmetric_requests") metricUserMetricsCalls = clientmetric.NewCounter("localapi_usermetric_requests") ) - -// serveSuggestExitNode serves a POST endpoint for returning a suggested exit node. -func (h *Handler) serveSuggestExitNode(w http.ResponseWriter, r *http.Request) { - if r.Method != "GET" { - http.Error(w, "only GET allowed", http.StatusMethodNotAllowed) - return - } - res, err := h.b.SuggestExitNode() - if err != nil { - writeErrorJSON(w, err) - return - } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(res) -} diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index 0bae8ef1b..26d155528 100644 --- a/wgengine/magicsock/magicsock.go +++ b/wgengine/magicsock/magicsock.go @@ -533,19 +533,6 @@ func NewConn(opts Options) (*Conn, error) { c.metrics = registerMetrics(opts.Metrics) - if d4, err := c.listenRawDisco("ip4"); err == nil { - c.logf("[v1] using BPF disco receiver for IPv4") - c.closeDisco4 = d4 - } else if !errors.Is(err, errors.ErrUnsupported) { - c.logf("[v1] couldn't create raw v4 disco listener, using regular listener instead: %v", err) - } - if d6, err := c.listenRawDisco("ip6"); err == nil { - c.logf("[v1] using BPF disco receiver for IPv6") - c.closeDisco6 = d6 - } else if !errors.Is(err, errors.ErrUnsupported) { - c.logf("[v1] couldn't create raw v6 disco listener, using regular listener instead: %v", err) - } - c.logf("magicsock: disco key = %v", c.discoShort) return c, nil } diff --git a/wgengine/magicsock/magicsock_linux.go b/wgengine/magicsock/magicsock_linux.go index c5df555cd..6ead4518d 100644 --- a/wgengine/magicsock/magicsock_linux.go +++ b/wgengine/magicsock/magicsock_linux.go @@ -4,17 +4,14 @@ package magicsock import ( - "bytes" "context" "encoding/binary" "errors" "fmt" - "io" "net" "net/netip" "strings" "syscall" - "time" "github.com/mdlayher/socket" "golang.org/x/net/bpf" @@ -24,7 +21,6 @@ import ( "golang.org/x/sys/unix" "tailscale.com/disco" "tailscale.com/envknob" - "tailscale.com/net/netns" "tailscale.com/types/ipproto" "tailscale.com/types/key" "tailscale.com/types/logger" @@ -164,117 +160,6 @@ var ( } ) -// listenRawDisco starts listening for disco packets on the given -// address family, which must be "ip4" or "ip6", using a raw socket -// and BPF filter. -// https://github.com/tailscale/tailscale/issues/3824 -func (c *Conn) listenRawDisco(family string) (io.Closer, error) { - if !envknobEnableRawDisco() { - // Return an 'errors.ErrUnsupported' to prevent the callee from - // logging; when we switch this to an opt-out (vs. an opt-in), - // drop the ErrUnsupported so that the callee logs that it was - // disabled. - return nil, fmt.Errorf("raw disco not enabled: %w", errors.ErrUnsupported) - } - - // https://github.com/tailscale/tailscale/issues/5607 - if !netns.UseSocketMark() { - return nil, errors.New("raw disco listening disabled, SO_MARK unavailable") - } - - var ( - udpnet string - addr string - proto int - testAddr netip.AddrPort - prog []bpf.Instruction - ) - switch family { - case "ip4": - udpnet = "udp4" - addr = "0.0.0.0" - proto = ethernetProtoIPv4() - testAddr = netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), 1) - prog = magicsockFilterV4 - case "ip6": - udpnet = "udp6" - addr = "::" - proto = ethernetProtoIPv6() - testAddr = netip.AddrPortFrom(netip.IPv6Loopback(), 1) - prog = magicsockFilterV6 - default: - return nil, fmt.Errorf("unsupported address family %q", family) - } - - asm, err := bpf.Assemble(prog) - if err != nil { - return nil, fmt.Errorf("assembling filter: %w", err) - } - - sock, err := socket.Socket( - unix.AF_PACKET, - unix.SOCK_DGRAM, - proto, - "afpacket", - nil, // no config - ) - if err != nil { - return nil, fmt.Errorf("creating AF_PACKET socket: %w", err) - } - - if err := sock.SetBPF(asm); err != nil { - sock.Close() - return nil, fmt.Errorf("installing BPF filter: %w", err) - } - - // If all the above succeeds, we should be ready to receive. Just - // out of paranoia, check that we do receive a well-formed disco - // packet. - tc, err := net.ListenPacket(udpnet, net.JoinHostPort(addr, "0")) - if err != nil { - sock.Close() - return nil, fmt.Errorf("creating disco test socket: %w", err) - } - defer tc.Close() - if _, err := tc.(*net.UDPConn).WriteToUDPAddrPort(testDiscoPacket, testAddr); err != nil { - sock.Close() - return nil, fmt.Errorf("writing disco test packet: %w", err) - } - - const selfTestTimeout = 100 * time.Millisecond - if err := sock.SetReadDeadline(time.Now().Add(selfTestTimeout)); err != nil { - sock.Close() - return nil, fmt.Errorf("setting socket timeout: %w", err) - } - - var ( - ctx = context.Background() - buf [1500]byte - ) - for { - n, _, err := sock.Recvfrom(ctx, buf[:], 0) - if err != nil { - sock.Close() - return nil, fmt.Errorf("reading during raw disco self-test: %w", err) - } - - _ /* src */, _ /* dst */, payload := parseUDPPacket(buf[:n], family == "ip6") - if payload == nil { - continue - } - if !bytes.Equal(payload, testDiscoPacket) { - c.discoLogf("listenRawDisco: self-test: received mismatched UDP packet of %d bytes", len(payload)) - continue - } - c.logf("[v1] listenRawDisco: self-test passed for %s", family) - break - } - sock.SetReadDeadline(time.Time{}) - - go c.receiveDisco(sock, family == "ip6") - return sock, nil -} - // parseUDPPacket is a basic parser for UDP packets that returns the source and // destination addresses, and the payload. The returned payload is a sub-slice // of the input buffer. diff --git a/wgengine/router/router_linux.go b/wgengine/router/router_linux.go index a6d748304..64e8459dc 100644 --- a/wgengine/router/router_linux.go +++ b/wgengine/router/router_linux.go @@ -549,106 +549,6 @@ func (r *linuxRouter) setNetfilterMode(mode preftype.NetfilterMode) error { switch mode { case netfilterOff: - switch r.netfilterMode { - case netfilterNoDivert: - if err := r.nfr.DelBase(); err != nil { - return err - } - if err := r.nfr.DelChains(); err != nil { - r.logf("note: %v", err) - // harmless, continue. - // This can happen if someone left a ref to - // this table somewhere else. - } - case netfilterOn: - if err := r.nfr.DelHooks(r.logf); err != nil { - return err - } - if err := r.nfr.DelBase(); err != nil { - return err - } - if err := r.nfr.DelChains(); err != nil { - r.logf("note: %v", err) - // harmless, continue. - // This can happen if someone left a ref to - // this table somewhere else. - } - } - r.snatSubnetRoutes = false - case netfilterNoDivert: - switch r.netfilterMode { - case netfilterOff: - reprocess = true - if err := r.nfr.AddChains(); err != nil { - return err - } - if err := r.nfr.AddBase(r.tunname); err != nil { - return err - } - if r.magicsockPortV4 != 0 { - if err := r.nfr.AddMagicsockPortRule(r.magicsockPortV4, "udp4"); err != nil { - return fmt.Errorf("could not add magicsock port rule v4: %w", err) - } - } - if r.magicsockPortV6 != 0 && r.getV6FilteringAvailable() { - if err := r.nfr.AddMagicsockPortRule(r.magicsockPortV6, "udp6"); err != nil { - return fmt.Errorf("could not add magicsock port rule v6: %w", err) - } - } - r.snatSubnetRoutes = false - case netfilterOn: - if err := r.nfr.DelHooks(r.logf); err != nil { - return err - } - } - case netfilterOn: - // Because of bugs in old version of iptables-compat, - // we can't add a "-j ts-forward" rule to FORWARD - // while ts-forward contains an "-m mark" rule. But - // we can add the row *before* populating ts-forward. - // So we have to delBase, then add the hooks, - // then re-addBase, just in case. - switch r.netfilterMode { - case netfilterOff: - reprocess = true - if err := r.nfr.AddChains(); err != nil { - return err - } - if err := r.nfr.DelBase(); err != nil { - return err - } - // AddHooks adds the ts loopback rule. - if err := r.nfr.AddHooks(); err != nil { - return err - } - // AddBase adds base ts rules - if err := r.nfr.AddBase(r.tunname); err != nil { - return err - } - if r.magicsockPortV4 != 0 { - if err := r.nfr.AddMagicsockPortRule(r.magicsockPortV4, "udp4"); err != nil { - return fmt.Errorf("could not add magicsock port rule v4: %w", err) - } - } - if r.magicsockPortV6 != 0 && r.getV6FilteringAvailable() { - if err := r.nfr.AddMagicsockPortRule(r.magicsockPortV6, "udp6"); err != nil { - return fmt.Errorf("could not add magicsock port rule v6: %w", err) - } - } - r.snatSubnetRoutes = false - case netfilterNoDivert: - reprocess = true - if err := r.nfr.DelBase(); err != nil { - return err - } - if err := r.nfr.AddHooks(); err != nil { - return err - } - if err := r.nfr.AddBase(r.tunname); err != nil { - return err - } - r.snatSubnetRoutes = false - } default: panic("unhandled netfilter mode") }