tailscale/tailscale · error

service is not being served in TUN mode

Error message

service is not being served in TUN mode

What it means

removeTunServe's second guard: the service exists in sc.Services but its Tun field is false, meaning it is served via web/TCP handlers rather than TUN mode. Deleting the Services entry here would rip out its non-TUN configuration, so the CLI refuses and tells you the mode mismatch.

Source

Thrown at cmd/tailscale/cli/serve_v2.go:1617

	}
	if sc.IsServingWeb(src, svcName) {
		return fmt.Errorf("unable to remove; serving web, not TCP forwarding on serve port %d", src)
	}
	sc.RemoveTCPForwarding(svcName, src)
	return nil
}

func (e *serveEnv) removeTunServe(sc *ipn.ServeConfig, dnsName string) error {
	if sc == nil {
		return nil
	}
	svcName := tailcfg.ServiceName(dnsName)
	svc, ok := sc.Services[svcName]
	if !ok || svc == nil {
		return errors.New("service does not exist")
	}
	if !svc.Tun {
		return errors.New("service is not being served in TUN mode")
	}
	delete(sc.Services, svcName)
	if len(sc.Services) == 0 {
		sc.Services = nil // clean up empty map
	}
	return nil
}

// cleanURLPath ensures the path is clean and has a leading "/".
func cleanURLPath(urlPath string) (string, error) {
	if urlPath == "" {
		return "/", nil
	}

	// TODO(tylersmalley) verify still needed with path being a flag
	urlPath = cleanMinGWPathConversionIfNeeded(urlPath)
	if !strings.HasPrefix(urlPath, "/") {
		urlPath = "/" + urlPath

View on GitHub (pinned to cfe32b8be6)

Solutions

  1. Check how the service is actually served: `tailscale serve status --service=svc:name`
  2. If it holds web/TCP handlers, remove those instead (e.g. `serve --https=443 off`, `--tcp=... off`)
  3. Only use the TUN-off flow for services whose config has Tun: true

Example fix

// before
$ # svc:myapp serves via web handlers, Tun=false
$ tailscale --service=svc:myapp serve tun off
service is not being served in TUN mode

// after
$ tailscale --service=svc:myapp serve --https=443 off
Defensive patterns

Strategy: validation

Validate before calling

if svc := sc.Services[svcName]; svc != nil && !svc.Tun {
	return fmt.Errorf("%s is served via web/TCP handlers; clear those instead", svcName)
}

Try / catch

if err := clearTun(svcName); err != nil {
	if strings.Contains(err.Error(), "not being served in TUN mode") {
		return clearWebHandlers(svcName) // fall through to the right mode
	}
	return err
}

Prevention

When it happens

Trigger: Calling the TUN-off path for a service that was advertised and configured only with web/TCP handlers (svc.Tun == false). Always checked after the service-exists guard, so hitting it implies the name was right but the mode assumption was wrong.

Common situations: Mixed setups where some services are TUN and some are proxied; scripts assuming every service is TUN; leftover commands after migrating a service from TUN to serve handlers.

Related errors


AI-assisted analysis of tailscale/tailscale@cfe32b8be6 (2026-08-15). Data as JSON: /api/errors/4b52fa61722cca0f. Report an issue: GitHub.