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 = "/" + urlPathView on GitHub (pinned to cfe32b8be6)
Solutions
- Check how the service is actually served: `tailscale serve status --service=svc:name`
- If it holds web/TCP handlers, remove those instead (e.g. `serve --https=443 off`, `--tcp=... off`)
- 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
- Branch on svc.Tun from the fetched serve config to pick the right off command
- Track each service's serving mode in your inventory so scripts don't assume TUN
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
- missing SessionID
- error: missing service name argument
- cannot specify both --all and --service
- must specify either --service=svc:<service-name> or --all
- must specify filename
AI-assisted analysis of tailscale/tailscale@cfe32b8be6 (2026-08-15).
Data as JSON: /api/errors/4b52fa61722cca0f.
Report an issue: GitHub.