package node import ( "bytes" "context" "encoding/json" "errors" "net/netip" "os" "os/exec" "time" ) type TailscaleStatus struct { Installed bool `json:"installed"` State string `json:"state"` Online bool `json:"online"` Addresses []string `json:"addresses"` } type boundedProviderOutput struct{ bytes.Buffer } func (b *boundedProviderOutput) Write(data []byte) (int, error) { if b.Len()+len(data) > 1024*1024 { return 0, errors.New("provider status too large") } return b.Buffer.Write(data) } func ReadTailscale() TailscaleStatus { value := TailscaleStatus{State: "not_installed", Addresses: []string{}} if _, err := os.Stat("/usr/bin/tailscale"); err != nil { return value } value.Installed = true value.State = "unavailable" ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() cmd := exec.CommandContext(ctx, "/usr/bin/tailscale", "status", "--json", "--peers=false") // Do not request peer inventory or expose auth URLs, user identities, keys, // provider diagnostics or profile objects in the product API. var output boundedProviderOutput cmd.Stdout = &output if err := cmd.Run(); err != nil { return value } return parseTailscale(output.Bytes()) } func parseTailscale(data []byte) TailscaleStatus { value := TailscaleStatus{Installed: true, State: "unavailable", Addresses: []string{}} if len(data) > 1024*1024 { return value } var raw struct { BackendState string TailscaleIPs []string Self *struct{ Online bool } } if json.Unmarshal(data, &raw) != nil { return value } switch raw.BackendState { case "Running", "Stopped", "NeedsLogin", "NeedsMachineAuth", "Starting", "NoState": value.State = raw.BackendState default: return value } value.Online = raw.BackendState == "Running" && raw.Self != nil && raw.Self.Online for _, address := range raw.TailscaleIPs { if ip, err := netip.ParseAddr(address); err == nil { value.Addresses = append(value.Addresses, ip.String()) } } return value }