package headscale import ( "bytes" "html/template" "net/http" textTemplate "text/template" "github.com/gofrs/uuid" "github.com/gorilla/mux" "github.com/rs/zerolog/log" ) // WindowsConfigMessage shows a simple message in the browser for how to configure the Windows Tailscale client. func (h *Headscale) WindowsConfigMessage( writer http.ResponseWriter, req *http.Request, ) { winTemplate := template.Must(template.New("windows").Parse(`

headscale

Windows registry configuration

This page provides Windows registry information for the official Windows Tailscale client.

The registry file will configure Tailscale to use {{.URL}} as its control server.

Caution

You should always download and inspect the registry file before installing it:

curl {{.URL}}/windows/tailscale.reg

Installation

Headscale can be set to the default server by running the registry file:

Windows registry file

  1. Download the registry file, then run it
  2. Follow the prompts
  3. Install and run the official windows Tailscale client
  4. When the installation has finished, start Tailscale, and log in by clicking the icon in the system tray

Or

Open command prompt with Administrator rights. Issue the following commands to add the required registry entries:

REG ADD "HKLM\Software\Tailscale IPN" /v UnattendedMode /t REG_SZ /d always
REG ADD "HKLM\Software\Tailscale IPN" /v LoginURL /t REG_SZ /d "{{.URL}}"

Restart Tailscale and log in.

`)) config := map[string]interface{}{ "URL": h.cfg.ServerURL, } var payload bytes.Buffer if err := winTemplate.Execute(&payload, config); err != nil { log.Error(). Str("handler", "WindowsRegConfig"). Err(err). Msg("Could not render Windows index template") writer.Header().Set("Content-Type", "text/plain; charset=utf-8") writer.WriteHeader(http.StatusInternalServerError) _, err := writer.Write([]byte("Could not render Windows index template")) if err != nil { log.Error(). Caller(). Err(err). Msg("Failed to write response") } return } writer.Header().Set("Content-Type", "text/html; charset=utf-8") writer.WriteHeader(http.StatusOK) _, err := writer.Write(payload.Bytes()) if err != nil { log.Error(). Caller(). Err(err). Msg("Failed to write response") } } // WindowsRegConfig generates and serves a .reg file configured with the Headscale server address. func (h *Headscale) WindowsRegConfig( writer http.ResponseWriter, req *http.Request, ) { config := WindowsRegistryConfig{ URL: h.cfg.ServerURL, } var content bytes.Buffer if err := windowsRegTemplate.Execute(&content, config); err != nil { log.Error(). Str("handler", "WindowsRegConfig"). Err(err). Msg("Could not render Apple macOS template") writer.Header().Set("Content-Type", "text/plain; charset=utf-8") writer.WriteHeader(http.StatusInternalServerError) _, err := writer.Write([]byte("Could not render Windows registry template")) if err != nil { log.Error(). Caller(). Err(err). Msg("Failed to write response") } return } writer.Header().Set("Content-Type", "text/x-ms-regedit; charset=utf-8") writer.WriteHeader(http.StatusOK) _, err := writer.Write(content.Bytes()) if err != nil { log.Error(). Caller(). Err(err). Msg("Failed to write response") } } // AppleConfigMessage shows a simple message in the browser to point the user to the iOS/MacOS profile and instructions for how to install it. func (h *Headscale) AppleConfigMessage( writer http.ResponseWriter, req *http.Request, ) { appleTemplate := template.Must(template.New("apple").Parse(`

headscale

Apple configuration profiles

This page provides configuration profiles for the official Tailscale clients for iOS and macOS.

The profiles will configure Tailscale.app to use {{.URL}} as its control server.

Caution

You should always download and inspect the profile before installing it:

curl {{.URL}}/apple/macos

Profiles

macOS

Headscale can be set to the default server by installing a Headscale configuration profile:

macOS profile

  1. Download the profile, then open it. When it has been opened, there should be a notification that a profile can be installed
  2. Open System Preferences and go to "Profiles"
  3. Find and install the Headscale profile
  4. Restart Tailscale.app and log in

Or

Use your terminal to configure the default setting for Tailscale by issuing:

defaults write io.tailscale.ipn.macos ControlURL {{.URL}}

Restart Tailscale.app and log in.

`)) config := map[string]interface{}{ "URL": h.cfg.ServerURL, } var payload bytes.Buffer if err := appleTemplate.Execute(&payload, config); err != nil { log.Error(). Str("handler", "AppleMobileConfig"). Err(err). Msg("Could not render Apple index template") writer.Header().Set("Content-Type", "text/plain; charset=utf-8") writer.WriteHeader(http.StatusInternalServerError) _, err := writer.Write([]byte("Could not render Apple index template")) if err != nil { log.Error(). Caller(). Err(err). Msg("Failed to write response") } return } writer.Header().Set("Content-Type", "text/html; charset=utf-8") writer.WriteHeader(http.StatusOK) _, err := writer.Write(payload.Bytes()) if err != nil { log.Error(). Caller(). Err(err). Msg("Failed to write response") } } func (h *Headscale) ApplePlatformConfig( writer http.ResponseWriter, req *http.Request, ) { vars := mux.Vars(req) platform, ok := vars["platform"] if !ok { log.Error(). Str("handler", "ApplePlatformConfig"). Msg("No platform specified") http.Error(writer, "No platform specified", http.StatusBadRequest) return } id, err := uuid.NewV4() if err != nil { log.Error(). Str("handler", "ApplePlatformConfig"). Err(err). Msg("Failed not create UUID") writer.Header().Set("Content-Type", "text/plain; charset=utf-8") writer.WriteHeader(http.StatusInternalServerError) _, err := writer.Write([]byte("Failed to create UUID")) if err != nil { log.Error(). Caller(). Err(err). Msg("Failed to write response") } return } contentID, err := uuid.NewV4() if err != nil { log.Error(). Str("handler", "ApplePlatformConfig"). Err(err). Msg("Failed not create UUID") writer.Header().Set("Content-Type", "text/plain; charset=utf-8") writer.WriteHeader(http.StatusInternalServerError) _, err := writer.Write([]byte("Failed to create content UUID")) if err != nil { log.Error(). Caller(). Err(err). Msg("Failed to write response") } return } platformConfig := AppleMobilePlatformConfig{ UUID: contentID, URL: h.cfg.ServerURL, } var payload bytes.Buffer switch platform { case "macos": if err := macosTemplate.Execute(&payload, platformConfig); err != nil { log.Error(). Str("handler", "ApplePlatformConfig"). Err(err). Msg("Could not render Apple macOS template") writer.Header().Set("Content-Type", "text/plain; charset=utf-8") writer.WriteHeader(http.StatusInternalServerError) _, err := writer.Write([]byte("Could not render Apple macOS template")) if err != nil { log.Error(). Caller(). Err(err). Msg("Failed to write response") } return } case "ios": if err := iosTemplate.Execute(&payload, platformConfig); err != nil { log.Error(). Str("handler", "ApplePlatformConfig"). Err(err). Msg("Could not render Apple iOS template") writer.Header().Set("Content-Type", "text/plain; charset=utf-8") writer.WriteHeader(http.StatusInternalServerError) _, err := writer.Write([]byte("Could not render Apple iOS template")) if err != nil { log.Error(). Caller(). Err(err). Msg("Failed to write response") } return } default: writer.Header().Set("Content-Type", "text/plain; charset=utf-8") writer.WriteHeader(http.StatusBadRequest) _, err := writer.Write([]byte("Invalid platform, only ios and macos is supported")) if err != nil { log.Error(). Caller(). Err(err). Msg("Failed to write response") } return } config := AppleMobileConfig{ UUID: id, URL: h.cfg.ServerURL, Payload: payload.String(), } var content bytes.Buffer if err := commonTemplate.Execute(&content, config); err != nil { log.Error(). Str("handler", "ApplePlatformConfig"). Err(err). Msg("Could not render Apple platform template") writer.Header().Set("Content-Type", "text/plain; charset=utf-8") writer.WriteHeader(http.StatusInternalServerError) _, err := writer.Write([]byte("Could not render Apple platform template")) if err != nil { log.Error(). Caller(). Err(err). Msg("Failed to write response") } return } writer.Header().Set("Content-Type", "application/x-apple-aspen-config; charset=utf-8") writer.WriteHeader(http.StatusOK) _, err = writer.Write(content.Bytes()) if err != nil { log.Error(). Caller(). Err(err). Msg("Failed to write response") } } type WindowsRegistryConfig struct { URL string } type AppleMobileConfig struct { UUID uuid.UUID URL string Payload string } type AppleMobilePlatformConfig struct { UUID uuid.UUID URL string } var windowsRegTemplate = textTemplate.Must( textTemplate.New("windowsconfig").Parse(`Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\SOFTWARE\Tailscale IPN] "UnattendedMode"="always" "LoginURL"="{{.URL}}" `)) var commonTemplate = textTemplate.Must( textTemplate.New("mobileconfig").Parse(` PayloadUUID {{.UUID}} PayloadDisplayName Headscale PayloadDescription Configure Tailscale login server to: {{.URL}} PayloadIdentifier com.github.juanfont.headscale PayloadRemovalDisallowed PayloadType Configuration PayloadVersion 1 PayloadContent {{.Payload}} `), ) var iosTemplate = textTemplate.Must(textTemplate.New("iosTemplate").Parse(` PayloadType io.tailscale.ipn.ios PayloadUUID {{.UUID}} PayloadIdentifier com.github.juanfont.headscale PayloadVersion 1 PayloadEnabled ControlURL {{.URL}} `)) var macosTemplate = template.Must(template.New("macosTemplate").Parse(` PayloadType io.tailscale.ipn.macos PayloadUUID {{.UUID}} PayloadIdentifier com.github.juanfont.headscale PayloadVersion 1 PayloadEnabled ControlURL {{.URL}} `))