package tailscale import ( "context" "encoding/json" "fmt" "os" "os/exec" "strconv" "strings" ) const cli = "/Applications/Tailscale.app/Contents/MacOS/Tailscale" func Serve(ctx context.Context, port int) error { _, err := serve(ctx, port, "http://127.0.0.1:"+strconv.Itoa(port)) return err } func ServeRoot(ctx context.Context, target string) (string, error) { host, err := serve(ctx, 443, target) if err != nil { return "", err } return "https://" + strings.TrimSuffix(host, ":443"), nil } func serve(ctx context.Context, port int, target string) (string, error) { _, err := run(ctx, "serve", "--bg", "--https="+strconv.Itoa(port), target) if err != nil { return "", err } out, err := run(ctx, "serve", "status", "--json") if err != nil { return "", err } status := struct { Web map[string]json.RawMessage }{} err = json.Unmarshal(out, &status) if err != nil { return "", fmt.Errorf("parse tailscale serve status: %w", err) } suffix := ":" + strconv.Itoa(port) for host := range status.Web { if strings.HasSuffix(host, suffix) { return host, nil } } return "", fmt.Errorf("tailscale serve --https=%d exited 0 but port %d is not in serve status", port, port) } func Off(ctx context.Context, port int) error { _, err := run(ctx, "serve", "--https="+strconv.Itoa(port), "off") return err } func run(ctx context.Context, args ...string) ([]byte, error) { cmd := exec.CommandContext(ctx, cli, args...) cmd.Env = append(os.Environ(), "TAILSCALE_BE_CLI=1") stderr := strings.Builder{} cmd.Stderr = &stderr out, err := cmd.Output() if err != nil { return nil, fmt.Errorf("tailscale %s: %w: %s", strings.Join(args, " "), err, strings.TrimSpace(stderr.String()+string(out))) } return out, nil }