{"record":{"id":"093386b7ac5a4f5e","repo":"k3s-io/k3s","slug":"vpn-error-invalid-control-server-url-for-tailscal","errorCode":null,"errorMessage":"VPN Error. Invalid control server URL for Tailscale: %w","messagePattern":"VPN Error\\. Invalid control server URL for Tailscale: %w","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/vpn/vpn.go","lineNumber":150,"sourceCode":"\t\t\treturn vpnCliAuthInfo{}, fmt.Errorf(\"VPN Error. The passed VPN auth info includes an unknown parameter: %v\", vpnKeyValue[0])\n\t\t}\n\t}\n\n\tif err := isVPNConfigOK(authInfo); err != nil {\n\t\treturn authInfo, err\n\t}\n\treturn authInfo, nil\n}\n\n// isVPNConfigOK checks that the config is complete\nfunc isVPNConfigOK(authInfo vpnCliAuthInfo) error {\n\tif authInfo.Name == \"tailscale\" {\n\t\tif authInfo.JoinKey == \"\" {\n\t\t\treturn errors.New(\"VPN Error. Tailscale requires a JoinKey\")\n\t\t}\n\t\tif authInfo.ControlServerURL != \"\" {\n\t\t\tif _, err := url.Parse(authInfo.ControlServerURL); err != nil {\n\t\t\t\treturn fmt.Errorf(\"VPN Error. Invalid control server URL for Tailscale: %w\", err)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\treturn errors.New(\"Requested VPN: \" + authInfo.Name + \" is not supported. We currently only support tailscale\")\n}\n\n// getTailscaleInfo returns the IPs of the interface\nfunc getTailscaleInfo() (*Info, error) {\n\toutput, err := util.ExecCommand(\"tailscale\", []string{\"status\", \"--json\"})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to run tailscale status --json: %v\", err)\n\t}\n\n\tlogrus.Debugf(\"Output from tailscale status --json: %v\", output)\n\n\tvar tailscaleOutput TailscaleOutput","sourceCodeStart":132,"sourceCodeEnd":168,"githubUrl":"https://github.com/k3s-io/k3s/blob/6ba341e396edc16b8dcae978a7c5e3ac7ee5606e/pkg/vpn/vpn.go#L132-L168","documentation":"isVPNConfigOK validates a tailscale auth config: joinKey must be non-empty, and if controlServerURL is set it must pass url.Parse. url.Parse fails only on genuinely malformed input (control characters, invalid bytes such as spaces in the host), so in practice this fires for garbled URLs - e.g. from bad templating - not merely wrong hostnames.","triggerScenarios":"A controlServerURL value containing control characters, newlines, or a space inside the host portion; a value mangled by templating or copy-paste.","commonSituations":"Values interpolated from env vars or Helm templates with stray whitespace/newlines; copy-paste artifacts; note that a syntactically valid but wrong hostname will NOT be caught here.","solutions":["Trim whitespace and newlines from the value before writing it into the auth string","Pre-test the value: url.Parse in Go, or curl -sI <url> to confirm it is reachable","Check connectivity to the login server separately - this validation only catches syntax, not reachability"],"exampleFix":"# before\ncontrolServerURL=https:// headscale.example.com\n# after\ncontrolServerURL=https://headscale.example.com","handlingStrategy":"validation","validationCode":"for _, seg := range strings.Split(vpnAuth, \",\") {\n\tparts := strings.SplitN(seg, \"=\", 2)\n\tif len(parts) == 2 && parts[0] == \"controlServerURL\" {\n\t\tv := strings.TrimSpace(parts[1])\n\t\tif _, err := url.Parse(v); err != nil {\n\t\t\treturn fmt.Errorf(\"invalid controlServerURL %q: %w\", v, err)\n\t\t}\n\t}\n}","typeGuard":"func isParsableURL(s string) bool {\n\ts = strings.TrimSpace(s)\n\t_, err := url.Parse(s)\n\treturn err == nil\n}","tryCatchPattern":"if err := vpn.StartVPN(authFile); err != nil {\n\tif strings.Contains(err.Error(), \"Invalid control server URL\") {\n\t\t// strip whitespace/newlines from the URL value in the auth config and retry\n\t}\n\treturn err\n}","preventionTips":["Trim whitespace and newlines from URLs before embedding them in config strings","Validate templated values with url.Parse at render time","Remember url.Parse only catches syntax; verify login-server reachability separately"],"tags":["go","vpn","url","validation","tailscale"],"backgroundTag":null,"analyzedSha":"6ba341e396edc16b8dcae978a7c5e3ac7ee5606e","analyzedAt":"2026-08-15T16:27:54.286Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}