k3s-io/k3s · error
failed to unmarshal tailscale output: %v
Error message
failed to unmarshal tailscale output: %v
What it means
The stdout of `tailscale status --json` is unmarshalled into TailscaleOutput{TailscaleIPs, BackendState}. If that output is not valid JSON for the shape - the CLI printed an error or warning instead, the output was empty, or a version changed the payload - json.Unmarshal fails and the error is wrapped here.
Source
Thrown at pkg/vpn/vpn.go:171
return nil
}
return errors.New("Requested VPN: " + authInfo.Name + " is not supported. We currently only support tailscale")
}
// getTailscaleInfo returns the IPs of the interface
func getTailscaleInfo() (*Info, error) {
output, err := util.ExecCommand("tailscale", []string{"status", "--json"})
if err != nil {
return nil, fmt.Errorf("failed to run tailscale status --json: %v", err)
}
logrus.Debugf("Output from tailscale status --json: %v", output)
var tailscaleOutput TailscaleOutput
err = json.Unmarshal([]byte(output), &tailscaleOutput)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal tailscale output: %v", err)
}
// Errors are ignored because the interface might not have ipv4 or ipv6 addresses (that's the only possible error)
ipv4Address, _ := util.GetFirst4String(tailscaleOutput.TailscaleIPs)
ipv6Address, _ := util.GetFirst6String(tailscaleOutput.TailscaleIPs)
return &Info{BackendState: tailscaleOutput.BackendState, IPv4Address: net.ParseIP(ipv4Address), IPv6Address: net.ParseIP(ipv6Address), NodeID: "", ProviderName: "tailscale", Interface: tailscaleIf}, nil
}
// get Tailscale advertised route list
func GetAdvertisedRoutes() ([]netip.Prefix, error) {
output, err := util.ExecCommand("tailscale", []string{"debug", "prefs"})
if err != nil {
return nil, fmt.Errorf("failed to run tailscale debug prefs: %v", err)
}
logrus.Debugf("Output from tailscale debug prefs: %v", output)
View on GitHub (pinned to 6ba341e396)
Solutions
- Run tailscale status --json by hand and pipe through jq . to see what is actually returned
- Align the CLI to a known-good version and restart tailscaled after the change
- If leading warnings pollute the output, fix the underlying warning (often permissions or stale state) first
Defensive patterns
Strategy: try-catch
Validate before calling
// pre-flight: confirm the CLI emits the JSON shape you expect
out, err := exec.Command("tailscale", "status", "--json").Output()
if err != nil {
return fmt.Errorf("tailscale status failed: %w", err)
}
var probe struct{ BackendState string `json:"BackendState"` }
if err := json.Unmarshal(out, &probe); err != nil {
return fmt.Errorf("unexpected tailscale output %q: %w", out, err)
} Try / catch
info, err := vpn.GetInfo(vpnAuth)
if err != nil {
if strings.Contains(err.Error(), "failed to unmarshal tailscale output") {
// run `tailscale status --json | jq .` by hand; usually version drift or
// non-JSON warnings on stdout - align the CLI version and restart tailscaled
log.Printf("raw tailscale output needs inspection")
}
return nil, err
} Prevention
- Pin the tailscale CLI version and restart tailscaled after upgrades
- Manually verify `tailscale status --json | jq .` after any CLI change
- Watch the tailscale release notes - output payloads do change between versions
When it happens
Trigger: tailscale exits 0 but writes non-JSON to stdout (warnings, banners, error text); an empty output; a CLI version whose `status --json` payload differs from the expected fields.
Common situations: Upgrading or downgrading the tailscale CLI without restarting tailscaled; distributions that patch tailscale; environments where the CLI wraps or pollutes stdout.
Related errors
- VPN Error. The passed VPN auth info includes an unknown para
- Requested VPN: %s is not supported. We currently only suppor
- VPN Error. Invalid control server URL for Tailscale: %w
- failed to run tailscale status --json: %v
- failed to run tailscale debug prefs: %v
AI-assisted analysis of k3s-io/k3s@6ba341e396 (2026-08-15).
Data as JSON: /api/errors/cf38daaa6e0b8a34.
Report an issue: GitHub.