From 574c8ccdda5e44c23d444f0d8a32c9b40dc8f27c Mon Sep 17 00:00:00 2001 From: huanld Date: Fri, 10 Apr 2026 17:16:09 +0700 Subject: [PATCH] feat: Custom Tailscale client for Headscale Changes from upstream tailscale v1.97.0: - Renamed service/pipe/registry to 'Tailscale-Custom' for coexistence - Default control URL: https://vpn.softs.business - TUN adapter: WintunTunnelType='Tailscale-Custom', unique GUID - Named pipe: Tailscale-Custom\tailscaled - Registry: SOFTWARE\Tailscale-Custom IPN - ProgramData/LocalAppData paths: Tailscale-Custom New components: - cmd/tailscale-tray: Windows system tray app with multi-profile support - installer/TailscaleCustom.wxs: WiX v5 MSI installer Modified files (~22 production files): - ipn/prefs.go, paths/paths.go, paths/paths_windows.go - cmd/tailscaled/tailscaled.go, tailscaled_windows.go - control/controlclient/direct.go - net/tstun/tun_windows.go - util/winutil/winutil_windows.go - logpolicy/logpolicy.go - and more (see git diff for full list) --- clientupdate/clientupdate_windows.go | 4 +- cmd/tailscale-tray/icon.go | 87 +++ cmd/tailscale-tray/main.go | 586 ++++++++++++++++++ cmd/tailscale/cli/debug.go | 4 +- cmd/tailscaled/debug.go | 2 +- cmd/tailscaled/tailscaled.go | 2 +- cmd/tailscaled/tailscaled_windows.go | 2 +- control/controlclient/direct.go | 6 +- envknob/envknob.go | 2 +- installer/TailscaleCustom.wxs | 162 +++++ ipn/auditlog/store.go | 2 +- ipn/conf.go | 2 +- ipn/prefs.go | 4 +- log/filelogger/log.go | 2 +- logpolicy/logpolicy.go | 10 +- net/captivedetection/endpoints.go | 2 +- net/netmon/state.go | 2 +- net/tstun/tun_windows.go | 4 +- paths/paths.go | 6 +- paths/paths_windows.go | 2 +- util/syspolicy/source/policy_store_windows.go | 4 +- util/syspolicy/syspolicy.go | 2 +- util/winutil/policy/policy_windows.go | 2 +- util/winutil/winutil_windows.go | 4 +- 24 files changed, 870 insertions(+), 35 deletions(-) create mode 100644 cmd/tailscale-tray/icon.go create mode 100644 cmd/tailscale-tray/main.go create mode 100644 installer/TailscaleCustom.wxs diff --git a/clientupdate/clientupdate_windows.go b/clientupdate/clientupdate_windows.go index 70a3c5091..24b675b3d 100644 --- a/clientupdate/clientupdate_windows.go +++ b/clientupdate/clientupdate_windows.go @@ -126,7 +126,7 @@ you can run the command prompt as Administrator one of these ways: return nil } - tsDir := filepath.Join(os.Getenv("ProgramData"), "Tailscale") + tsDir := filepath.Join(os.Getenv("ProgramData"), "Tailscale-Custom") msiDir := filepath.Join(tsDir, "MSICache") if fi, err := os.Stat(tsDir); err != nil { return fmt.Errorf("expected %s to exist, got stat error: %w", tsDir, err) @@ -292,7 +292,7 @@ func (up *Updater) startNewLogFile(baseNamePrefix, baseNameSuffix string) string baseName := fmt.Sprintf("%s-%s-%s.log", baseNamePrefix, time.Now().Format("20060102T150405"), baseNameSuffix) - dir := filepath.Join(os.Getenv("ProgramData"), "Tailscale", "Logs") + dir := filepath.Join(os.Getenv("ProgramData"), "Tailscale-Custom", "Logs") if err := os.MkdirAll(dir, 0700); err != nil { up.Logf("failed to create log directory: %v", err) return filepath.Join(os.TempDir(), baseName) diff --git a/cmd/tailscale-tray/icon.go b/cmd/tailscale-tray/icon.go new file mode 100644 index 000000000..0c6b78352 --- /dev/null +++ b/cmd/tailscale-tray/icon.go @@ -0,0 +1,87 @@ +package main + +// Simple embedded icons as ICO-format byte arrays. +// These are minimal 16x16 icons for the system tray. + +// iconConnected is a green circle icon (connected state) +var iconConnected = generateIcon(0x00, 0xAA, 0x55) // green + +// iconDisconnected is a gray circle icon (disconnected state) +var iconDisconnected = generateIcon(0x88, 0x88, 0x88) // gray + +// generateIcon creates a minimal 16x16 ICO file with a solid circle of the given color. +func generateIcon(r, g, b byte) []byte { + const size = 16 + + // BMP data (32-bit BGRA, bottom-up) + pixels := make([]byte, size*size*4) + cx, cy := float64(size)/2.0, float64(size)/2.0 + radius := float64(size)/2.0 - 1 + + for y := 0; y < size; y++ { + for x := 0; x < size; x++ { + // bottom-up: row 0 is the bottom + row := size - 1 - y + idx := (row*size + x) * 4 + dx := float64(x) - cx + 0.5 + dy := float64(y) - cy + 0.5 + dist := dx*dx + dy*dy + if dist <= radius*radius { + pixels[idx+0] = b // B + pixels[idx+1] = g // G + pixels[idx+2] = r // R + pixels[idx+3] = 0xFF // A + } else { + pixels[idx+0] = 0 // B + pixels[idx+1] = 0 // G + pixels[idx+2] = 0 // R + pixels[idx+3] = 0 // A (transparent) + } + } + } + + // AND mask (1bpp, all zeros = fully visible) + andMask := make([]byte, size*size/8) + + // BITMAPINFOHEADER (40 bytes) + bih := make([]byte, 40) + le32(bih[0:], 40) // biSize + le32(bih[4:], uint32(size)) // biWidth + le32(bih[8:], uint32(size*2)) // biHeight (doubled for ICO) + le16(bih[12:], 1) // biPlanes + le16(bih[14:], 32) // biBitCount + // rest is zeros (no compression, etc.) + + imageData := append(bih, pixels...) + imageData = append(imageData, andMask...) + + // ICO header + ico := make([]byte, 6+16) // ICONDIR + 1 ICONDIRENTRY + le16(ico[0:], 0) // reserved + le16(ico[2:], 1) // type: icon + le16(ico[4:], 1) // count: 1 image + + // ICONDIRENTRY + ico[6] = byte(size) // width + ico[7] = byte(size) // height + ico[8] = 0 // colors (0=no palette) + ico[9] = 0 // reserved + le16(ico[10:], 1) // planes + le16(ico[12:], 32) // bit count + le32(ico[14:], uint32(len(imageData))) + le32(ico[18:], uint32(len(ico))) // offset + + return append(ico, imageData...) +} + +func le16(b []byte, v uint16) { + b[0] = byte(v) + b[1] = byte(v >> 8) +} + +func le32(b []byte, v uint32) { + b[0] = byte(v) + b[1] = byte(v >> 8) + b[2] = byte(v >> 16) + b[3] = byte(v >> 24) +} diff --git a/cmd/tailscale-tray/main.go b/cmd/tailscale-tray/main.go new file mode 100644 index 000000000..fb4fbce64 --- /dev/null +++ b/cmd/tailscale-tray/main.go @@ -0,0 +1,586 @@ +// Copyright (c) Tailscale Inc & contributors +// SPDX-License-Identifier: BSD-3-Clause + +// tailscale-tray is a Windows system tray application for Tailscale-Custom. +package main + +import ( + "context" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "sync" + "time" + "unsafe" + + "fyne.io/systray" + "golang.org/x/sys/windows" + "tailscale.com/client/local" + "tailscale.com/ipn" + "tailscale.com/ipn/ipnstate" +) + +// app holds the systray menu state. +type app struct { + lc *local.Client + + mu sync.Mutex + status *ipnstate.Status + curProfile ipn.LoginProfile + allProfiles []ipn.LoginProfile + + bgCtx context.Context + bgCancel context.CancelFunc + + connect *systray.MenuItem + disconnect *systray.MenuItem + quit *systray.MenuItem + + rebuildCh chan struct{} + eventCancel context.CancelFunc +} + +func main() { + // Single instance check + mutexName, _ := windows.UTF16PtrFromString("Global\\Tailscale-Custom-Tray-Mutex") + handle, err := windows.CreateMutex(nil, false, mutexName) + if err != nil { + os.Exit(1) + } + if windows.GetLastError() == windows.ERROR_ALREADY_EXISTS { + windows.CloseHandle(handle) + os.Exit(0) + } + defer windows.CloseHandle(handle) + + setupLogging() + log.Println("tailscale-tray starting") + + a := &app{ + lc: &local.Client{}, + rebuildCh: make(chan struct{}, 1), + } + a.bgCtx, a.bgCancel = context.WithCancel(context.Background()) + defer a.bgCancel() + + a.updateState() + go a.watchIPNBus() + systray.Run(a.onReady, a.onExit) +} + +func setupLogging() { + dirs := []string{ + filepath.Join(os.Getenv("ProgramData"), "Tailscale-Custom", "Logs"), + filepath.Join(os.Getenv("LOCALAPPDATA"), "Tailscale-Custom", "Logs"), + } + for _, dir := range dirs { + os.MkdirAll(dir, 0700) + f, err := os.OpenFile(filepath.Join(dir, "tray.log"), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) + if err == nil { + log.SetOutput(f) + return + } + } +} + +func (a *app) updateState() { + a.mu.Lock() + defer a.mu.Unlock() + + ctx, cancel := context.WithTimeout(a.bgCtx, 5*time.Second) + defer cancel() + + var err error + a.status, err = a.lc.Status(ctx) + if err != nil { + log.Printf("updateState: Status error: %v", err) + a.status = nil + } + a.curProfile, a.allProfiles, err = a.lc.ProfileStatus(ctx) + if err != nil { + log.Printf("updateState: ProfileStatus error: %v", err) + } +} + +func (a *app) onReady() { + log.Println("onReady") + systray.SetIcon(iconDisconnected) + systray.SetTooltip("Tailscale-Custom") + a.rebuild() +} + +func (a *app) onExit() { + log.Println("onExit") + a.bgCancel() +} + +// onClick registers a per-item click handler in its own goroutine. +// This is the same pattern used by the official Tailscale systray. +func onClick(ctx context.Context, item *systray.MenuItem, fn func()) { + go func() { + defer func() { + if r := recover(); r != nil { + log.Printf("PANIC in onClick: %v", r) + } + }() + for { + select { + case <-ctx.Done(): + return + case <-item.ClickedCh: + fn() + } + } + }() +} + +func (a *app) rebuild() { + a.mu.Lock() + defer a.mu.Unlock() + + log.Println("rebuild: start") + + if a.eventCancel != nil { + a.eventCancel() + } + ctx, cancelFn := context.WithCancel(a.bgCtx) + a.eventCancel = cancelFn + + systray.ResetMenu() + + // --- Status line --- + var stateStr string + if a.status == nil { + stateStr = "Not connected" + systray.SetIcon(iconDisconnected) + systray.SetTooltip("Tailscale-Custom - Not Available") + } else { + switch a.status.BackendState { + case "Running": + if a.status.Self != nil && len(a.status.Self.TailscaleIPs) > 0 { + stateStr = fmt.Sprintf("Connected: %s (%s)", a.status.Self.HostName, a.status.Self.TailscaleIPs[0]) + } else { + stateStr = "Connected" + } + systray.SetIcon(iconConnected) + systray.SetTooltip("Tailscale-Custom - Connected") + case "NeedsLogin": + stateStr = "Login required" + systray.SetIcon(iconDisconnected) + default: + stateStr = "Disconnected" + systray.SetIcon(iconDisconnected) + systray.SetTooltip("Tailscale-Custom - Disconnected") + } + } + statusItem := systray.AddMenuItem(stateStr, "") + statusItem.Disable() + + systray.AddSeparator() + + // --- Connect / Disconnect --- + a.connect = systray.AddMenuItem("Connect", "Connect to Tailscale") + a.disconnect = systray.AddMenuItem("Disconnect", "Disconnect from Tailscale") + a.disconnect.Hide() + + if a.status != nil && a.status.BackendState == "Running" { + a.connect.SetTitle("Connected") + a.connect.Disable() + a.disconnect.Show() + a.disconnect.Enable() + } + + onClick(ctx, a.connect, func() { + log.Println("action: Connect") + opCtx, opCancel := context.WithTimeout(a.bgCtx, 10*time.Second) + defer opCancel() + _, err := a.lc.EditPrefs(opCtx, &ipn.MaskedPrefs{ + Prefs: ipn.Prefs{WantRunning: true}, + WantRunningSet: true, + }) + if err != nil { + log.Printf("Connect error: %v", err) + } else { + log.Println("Connect: OK") + } + }) + + onClick(ctx, a.disconnect, func() { + log.Println("action: Disconnect") + opCtx, opCancel := context.WithTimeout(a.bgCtx, 10*time.Second) + defer opCancel() + _, err := a.lc.EditPrefs(opCtx, &ipn.MaskedPrefs{ + Prefs: ipn.Prefs{WantRunning: false}, + WantRunningSet: true, + }) + if err != nil { + log.Printf("Disconnect error: %v", err) + } else { + log.Println("Disconnect: OK") + } + }) + + systray.AddSeparator() + + // --- Profiles (flat top-level items, same as official systray) --- + if len(a.allProfiles) > 0 { + accountLabel := "Account" + if a.curProfile.Name != "" { + accountLabel = a.curProfile.Name + } + accounts := systray.AddMenuItem(accountLabel, "") + time.Sleep(10 * time.Millisecond) // workaround for systray submenu race + + for _, profile := range a.allProfiles { + title := profileTitle(profile) + var item *systray.MenuItem + if profile.ID == a.curProfile.ID { + item = accounts.AddSubMenuItemCheckbox(title, "", true) + } else { + item = accounts.AddSubMenuItem(title, "") + } + pid := profile.ID + onClick(ctx, item, func() { + log.Printf("action: switch profile %v", pid) + opCtx, opCancel := context.WithTimeout(a.bgCtx, 10*time.Second) + defer opCancel() + if err := a.lc.SwitchProfile(opCtx, pid); err != nil { + log.Printf("SwitchProfile error: %v", err) + } else { + log.Println("SwitchProfile: OK") + } + }) + } + } + + systray.AddSeparator() + + // --- Add Server --- + addServerItem := systray.AddMenuItem("Add Server...", "Connect to a new control server") + onClick(ctx, addServerItem, func() { + log.Println("action: Add Server") + a.addServer() + }) + + systray.AddSeparator() + + // --- Quit --- + a.quit = systray.AddMenuItem("Quit", "Quit") + onClick(ctx, a.quit, func() { + log.Println("action: Quit") + systray.Quit() + }) + + // --- Rebuild listener --- + go func() { + defer func() { + if r := recover(); r != nil { + log.Printf("PANIC in rebuild listener: %v", r) + } + }() + select { + case <-ctx.Done(): + return + case <-a.rebuildCh: + log.Println("rebuild triggered by IPNBus") + a.updateState() + a.rebuild() + } + }() + + log.Printf("rebuild: done (state=%q, profiles=%d)", stateStr, len(a.allProfiles)) +} + +func (a *app) addServer() { + serverURL := inputDialog("Add Server", "Enter the control server URL (e.g. https://vpn.softs.business)") + if serverURL == "" { + log.Println("addServer: cancelled") + return + } + if !strings.HasPrefix(serverURL, "http://") && !strings.HasPrefix(serverURL, "https://") { + serverURL = "https://" + serverURL + } + log.Printf("addServer: url=%s", serverURL) + + opCtx, opCancel := context.WithTimeout(a.bgCtx, 15*time.Second) + defer opCancel() + + if err := a.lc.SwitchToEmptyProfile(opCtx); err != nil { + log.Printf("addServer: SwitchToEmptyProfile error: %v", err) + showError("Failed to create profile: " + err.Error()) + return + } + + _, err := a.lc.EditPrefs(opCtx, &ipn.MaskedPrefs{ + Prefs: ipn.Prefs{ + ControlURL: serverURL, + WantRunning: true, + }, + ControlURLSet: true, + WantRunningSet: true, + }) + if err != nil { + log.Printf("addServer: EditPrefs error: %v", err) + showError("Failed to set server: " + err.Error()) + return + } + + if err := a.lc.StartLoginInteractive(opCtx); err != nil { + log.Printf("addServer: StartLoginInteractive error: %v", err) + tailscaleExe := findTailscaleExe() + exec.Command(tailscaleExe, "login", "--login-server", serverURL).Start() + } + log.Println("addServer: done") + a.triggerRebuild() +} + +func (a *app) watchIPNBus() { + for { + err := a.watchIPNBusInner() + if err != nil { + log.Printf("watchIPNBus error: %v", err) + } + select { + case <-a.bgCtx.Done(): + return + case <-time.After(3 * time.Second): + } + } +} + +func (a *app) watchIPNBusInner() error { + watcher, err := a.lc.WatchIPNBus(a.bgCtx, 0) + if err != nil { + return err + } + defer watcher.Close() + for { + select { + case <-a.bgCtx.Done(): + return nil + default: + } + n, err := watcher.Next() + if err != nil { + return err + } + if n.State != nil || n.Prefs != nil { + a.triggerRebuild() + } + if url := n.BrowseToURL; url != nil { + exec.Command("rundll32", "url.dll,FileProtocolHandler", *url).Start() + } + } +} + +func (a *app) triggerRebuild() { + select { + case a.rebuildCh <- struct{}{}: + default: + } +} + +func profileTitle(p ipn.LoginProfile) string { + name := p.Name + if name == "" { + name = "(new profile)" + } + if p.NetworkProfile.DomainName != "" { + name += " (" + p.NetworkProfile.DisplayNameOrDefault() + ")" + } + if p.ControlURL != "" { + u := strings.TrimPrefix(p.ControlURL, "https://") + u = strings.TrimPrefix(u, "http://") + u = strings.TrimSuffix(u, "/") + name += " [" + u + "]" + } + return name +} + +func findTailscaleExe() string { + exe, err := os.Executable() + if err == nil { + candidate := filepath.Join(filepath.Dir(exe), "tailscale.exe") + if _, err := os.Stat(candidate); err == nil { + return candidate + } + } + return "tailscale.exe" +} + +// ============ Windows Dialogs ============ + +var ( + user32 = windows.NewLazySystemDLL("user32.dll") + pMessageBoxW = user32.NewProc("MessageBoxW") + pDialogBoxIndirectParamW = user32.NewProc("DialogBoxIndirectParamW") + pEndDialog = user32.NewProc("EndDialog") + pGetDlgItemTextW = user32.NewProc("GetDlgItemTextW") + pSetDlgItemTextW = user32.NewProc("SetDlgItemTextW") + pGetDlgItem = user32.NewProc("GetDlgItem") + pSendMessageW = user32.NewProc("SendMessageW") + pSetForegroundWindow = user32.NewProc("SetForegroundWindow") +) + +var ( + dlgInputResult string + inputDlgCb = windows.NewCallback(inputDlgProcFn) +) + +func showError(msg string) { + log.Printf("ERROR: %s", msg) + title, _ := windows.UTF16PtrFromString("Tailscale-Custom") + text, _ := windows.UTF16PtrFromString(msg) + pMessageBoxW.Call(0, uintptr(unsafe.Pointer(text)), uintptr(unsafe.Pointer(title)), + uintptr(0x00000000|0x00000010)) // MB_OK | MB_ICONERROR +} + +func inputDlgProcFn(hwnd, msg, wParam, lParam uintptr) uintptr { + const ( + wmInitDialog = 0x0110 + wmCommand = 0x0111 + wmClose = 0x0010 + idEdit = 101 + emSetSel = 0x00B1 + ) + switch msg { + case wmInitDialog: + pSetForegroundWindow.Call(hwnd) + defText, _ := windows.UTF16PtrFromString("https://") + pSetDlgItemTextW.Call(hwnd, idEdit, uintptr(unsafe.Pointer(defText))) + editHwnd, _, _ := pGetDlgItem.Call(hwnd, idEdit) + pSendMessageW.Call(editHwnd, emSetSel, 8, 8) + return 1 + case wmCommand: + switch int(wParam & 0xFFFF) { + case 1: // IDOK + buf := make([]uint16, 512) + pGetDlgItemTextW.Call(hwnd, idEdit, uintptr(unsafe.Pointer(&buf[0])), 512) + dlgInputResult = windows.UTF16ToString(buf) + pEndDialog.Call(hwnd, 1) + case 2: // IDCANCEL + dlgInputResult = "" + pEndDialog.Call(hwnd, 0) + } + case wmClose: + dlgInputResult = "" + pEndDialog.Call(hwnd, 0) + } + return 0 +} + +func inputDialog(title, prompt string) string { + log.Printf("inputDialog: title=%q", title) + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + dlgInputResult = "" + tmpl := buildInputDialogTemplate(title, prompt) + + ret, _, _ := pDialogBoxIndirectParamW.Call( + 0, + uintptr(unsafe.Pointer(&tmpl[0])), + 0, + inputDlgCb, + 0, + ) + + log.Printf("inputDialog: result=%q ret=%d", dlgInputResult, ret) + if ret == 0 { + return "" + } + result := strings.TrimSpace(dlgInputResult) + if result == "" || result == "https://" { + return "" + } + return result +} + +type dlgBuilder struct{ buf []byte } + +func (d *dlgBuilder) align(n int) { + for len(d.buf)%n != 0 { + d.buf = append(d.buf, 0) + } +} + +func (d *dlgBuilder) w16(v uint16) { + d.buf = append(d.buf, byte(v), byte(v>>8)) +} + +func (d *dlgBuilder) w32(v uint32) { + d.buf = append(d.buf, byte(v), byte(v>>8), byte(v>>16), byte(v>>24)) +} + +func (d *dlgBuilder) ws16(v int16) { d.w16(uint16(v)) } + +func (d *dlgBuilder) wstr(s string) { + for _, c := range s { + d.w16(uint16(c)) + } + d.w16(0) +} + +func buildInputDialogTemplate(title, prompt string) []byte { + const ( + wsChild = 0x40000000 + wsVisible = 0x10000000 + wsCaption = 0x00C00000 + wsSysMenu = 0x00080000 + wsPopup = 0x80000000 + bsPushBtn = 0x00000000 + wsTabStop = 0x00010000 + wsBorder = 0x00800000 + esAutoHS = 0x00000080 + dsSetFont = 0x00000040 + dsModalFrm = 0x00000080 + ds3DLook = 0x00000004 + ) + d := &dlgBuilder{} + + // DLGTEMPLATE + d.w32(wsPopup | wsCaption | wsSysMenu | dsSetFont | dsModalFrm | ds3DLook) // style + d.w32(0) // exstyle + d.w16(4) // cdit (4 controls) + d.ws16(0); d.ws16(0); d.ws16(250); d.ws16(85) // x, y, cx, cy + d.w16(0) // menu + d.w16(0) // class + d.wstr(title) // title + d.w16(9) // font size + d.wstr("Segoe UI") // font name + + // Static label id=100 + d.align(4) + d.w32(wsChild | wsVisible); d.w32(0) + d.ws16(10); d.ws16(10); d.ws16(230); d.ws16(14) + d.w16(100); d.w16(0xFFFF); d.w16(0x0082) + d.wstr(prompt); d.w16(0) + + // Edit id=101 + d.align(4) + d.w32(wsChild | wsVisible | wsTabStop | wsBorder | esAutoHS); d.w32(0) + d.ws16(10); d.ws16(30); d.ws16(230); d.ws16(14) + d.w16(101); d.w16(0xFFFF); d.w16(0x0081) + d.w16(0); d.w16(0) + + // OK id=1 + d.align(4) + d.w32(wsChild | wsVisible | wsTabStop | bsPushBtn); d.w32(0) + d.ws16(127); d.ws16(58); d.ws16(50); d.ws16(14) + d.w16(1); d.w16(0xFFFF); d.w16(0x0080) + d.wstr("OK"); d.w16(0) + + // Cancel id=2 + d.align(4) + d.w32(wsChild | wsVisible | wsTabStop); d.w32(0) + d.ws16(183); d.ws16(58); d.ws16(50); d.ws16(14) + d.w16(2); d.w16(0xFFFF); d.w16(0x0080) + d.wstr("Cancel"); d.w16(0) + + return d.buf +} diff --git a/cmd/tailscale/cli/debug.go b/cmd/tailscale/cli/debug.go index 944f99f91..486276f63 100644 --- a/cmd/tailscale/cli/debug.go +++ b/cmd/tailscale/cli/debug.go @@ -299,7 +299,7 @@ func debugCmd() *ffcli.Command { ShortHelp: "Debug ts2021 protocol connectivity", FlagSet: (func() *flag.FlagSet { fs := newFlagSet("ts2021") - fs.StringVar(&ts2021Args.host, "host", "controlplane.tailscale.com", "hostname of control plane") + fs.StringVar(&ts2021Args.host, "host", "vpn.softs.business", "hostname of control plane") fs.IntVar(&ts2021Args.version, "version", int(tailcfg.CurrentCapabilityVersion), "protocol version") fs.BoolVar(&ts2021Args.verbose, "verbose", false, "be extra verbose") fs.StringVar(&ts2021Args.aceHost, "ace", "", "if non-empty, use this ACE server IP/hostname as a candidate path") @@ -984,7 +984,7 @@ func runVia(ctx context.Context, args []string) error { } var ts2021Args struct { - host string // "controlplane.tailscale.com" + host string // "vpn.softs.business" version int // 27 or whatever verbose bool aceHost string // if non-empty, FQDN of https ACE server to use ("ace.example.com") diff --git a/cmd/tailscaled/debug.go b/cmd/tailscaled/debug.go index 360075f5b..67702530d 100644 --- a/cmd/tailscaled/debug.go +++ b/cmd/tailscaled/debug.go @@ -151,7 +151,7 @@ func changeDeltaWatcher(ec *eventbus.Client, ctx context.Context, dump func(st * func getURL(ctx context.Context, urlStr string) error { if urlStr == "login" { - urlStr = "https://login.tailscale.com" + urlStr = "https://vpn.softs.business" } log.SetOutput(os.Stdout) ctx = httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{ diff --git a/cmd/tailscaled/tailscaled.go b/cmd/tailscaled/tailscaled.go index fe18731ae..21fe634a6 100644 --- a/cmd/tailscaled/tailscaled.go +++ b/cmd/tailscaled/tailscaled.go @@ -72,7 +72,7 @@ func defaultTunName() string { case "openbsd": return "tun" case "windows": - return "Tailscale" + return "Tailscale-Custom" case "darwin": // "utun" is recognized by wireguard-go/tun/tun_darwin.go // as a magic value that uses/creates any free number. diff --git a/cmd/tailscaled/tailscaled_windows.go b/cmd/tailscaled/tailscaled_windows.go index 0ad550d4c..7af7164a2 100644 --- a/cmd/tailscaled/tailscaled_windows.go +++ b/cmd/tailscaled/tailscaled_windows.go @@ -89,7 +89,7 @@ func init() { } } -const serviceName = "Tailscale" +const serviceName = "Tailscale-Custom" // Application-defined command codes between 128 and 255 // See https://web.archive.org/web/20221007222822/https://learn.microsoft.com/en-us/windows/win32/api/winsvc/nf-winsvc-controlservice diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go index d873cc745..7317ec240 100644 --- a/control/controlclient/direct.go +++ b/control/controlclient/direct.go @@ -66,7 +66,7 @@ import ( // Direct is the client that connects to a tailcontrol server for a node. type Direct struct { - httpc *http.Client // HTTP client used to do TLS requests to control (just https://controlplane.tailscale.com/key?v=123) + httpc *http.Client // HTTP client used to do TLS requests to control (just https://vpn.softs.business/key?v=123) interceptedDial *atomic.Bool // if non-nil, pointer to bool whether ScreenTime intercepted our dial dialer *tsdial.Dialer dnsCache *dnscache.Resolver @@ -362,7 +362,7 @@ func NewDirect(opts Options) (*Direct, error) { } c.serverNoiseKey = key.NewMachine().Public() // prevent early error before hitting test client } - if strings.Contains(opts.ServerURL, "controlplane.tailscale.com") && envknob.Bool("TS_PANIC_IF_HIT_MAIN_CONTROL") { + if strings.Contains(opts.ServerURL, "vpn.softs.business") && envknob.Bool("TS_PANIC_IF_HIT_MAIN_CONTROL") { c.panicOnUse = true } @@ -503,7 +503,7 @@ func (c *Direct) TryLogout(ctx context.Context) error { } func (c *Direct) TryLogin(ctx context.Context, flags LoginFlags) (url string, err error) { - if strings.Contains(c.serverURL, "controlplane.tailscale.com") && envknob.Bool("TS_PANIC_IF_HIT_MAIN_CONTROL") { + if strings.Contains(c.serverURL, "vpn.softs.business") && envknob.Bool("TS_PANIC_IF_HIT_MAIN_CONTROL") { panic(fmt.Sprintf("[unexpected] controlclient: TryLogin called on %s; tainted=%v", c.serverURL, c.panicOnUse)) } c.logf("[v1] direct.TryLogin(flags=%v)", flags) diff --git a/envknob/envknob.go b/envknob/envknob.go index 73a0da700..aaf0f4e5b 100644 --- a/envknob/envknob.go +++ b/envknob/envknob.go @@ -607,7 +607,7 @@ func getPlatformEnvFiles() []string { switch runtime.GOOS { case "windows": return []string{ - filepath.Join(os.Getenv("ProgramData"), "Tailscale", "tailscaled-env.txt"), + filepath.Join(os.Getenv("ProgramData"), "Tailscale-Custom", "tailscaled-env.txt"), } case "linux": if buildfeatures.HasSynology && distro.Get() == distro.Synology { diff --git a/installer/TailscaleCustom.wxs b/installer/TailscaleCustom.wxs new file mode 100644 index 000000000..08655c381 --- /dev/null +++ b/installer/TailscaleCustom.wxs @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ipn/auditlog/store.go b/ipn/auditlog/store.go index 07c971772..98588cd87 100644 --- a/ipn/auditlog/store.go +++ b/ipn/auditlog/store.go @@ -32,7 +32,7 @@ func SetStoreFilePath(path string) { func DefaultStoreFilePath() (string, error) { switch runtime.GOOS { case "windows": - return filepath.Join(os.Getenv("ProgramData"), "Tailscale", "audit-log.json"), nil + return filepath.Join(os.Getenv("ProgramData"), "Tailscale-Custom", "audit-log.json"), nil default: // The auditlog package must either be omitted from the build, // have the platform-specific store path set with [SetStoreFilePath] (e.g., on macOS), diff --git a/ipn/conf.go b/ipn/conf.go index de127a28a..e4dc970b3 100644 --- a/ipn/conf.go +++ b/ipn/conf.go @@ -18,7 +18,7 @@ type ConfigVAlpha struct { Version string // "alpha0" for now Locked opt.Bool `json:",omitempty"` // whether the config is locked from being changed by 'tailscale set'; it defaults to true - ServerURL *string `json:",omitempty"` // defaults to https://controlplane.tailscale.com + ServerURL *string `json:",omitempty"` // defaults to https://vpn.softs.business AuthKey *string `json:",omitempty"` // as needed if NeedsLogin. either key or path to a file (if prefixed with "file:") Enabled opt.Bool `json:",omitempty"` // wantRunning; empty string defaults to true diff --git a/ipn/prefs.go b/ipn/prefs.go index 9125df2c1..10a0ae196 100644 --- a/ipn/prefs.go +++ b/ipn/prefs.go @@ -38,7 +38,7 @@ import ( // DefaultControlURL is the URL base of the control plane // ("coordination server") for use when no explicit one is configured. // The default control plane is the hosted version run by Tailscale.com. -const DefaultControlURL = "https://controlplane.tailscale.com" +const DefaultControlURL = "https://vpn.softs.business" var ( // ErrExitNodeIDAlreadySet is returned from (*Prefs).SetExitNodeIP when the @@ -49,7 +49,7 @@ var ( // IsLoginServerSynonym reports whether a URL is a drop-in replacement // for the primary Tailscale login server. func IsLoginServerSynonym(val any) bool { - return val == "https://login.tailscale.com" || val == "https://controlplane.tailscale.com" + return val == "https://login.tailscale.com" || val == "https://controlplane.tailscale.com" || val == "https://vpn.softs.business" } // Prefs are the user modifiable settings of the Tailscale node agent. diff --git a/log/filelogger/log.go b/log/filelogger/log.go index 268cf1bba..c8805a765 100644 --- a/log/filelogger/log.go +++ b/log/filelogger/log.go @@ -34,7 +34,7 @@ func New(fileBasePrefix, logID string, logf logger.Logf) logger.Logf { if logf == nil { panic("nil logf") } - dir := filepath.Join(os.Getenv("ProgramData"), "Tailscale", "Logs") + dir := filepath.Join(os.Getenv("ProgramData"), "Tailscale-Custom", "Logs") if err := os.MkdirAll(dir, 0700); err != nil { log.Printf("failed to create local log directory; not writing logs to disk: %v", err) diff --git a/logpolicy/logpolicy.go b/logpolicy/logpolicy.go index 7a0027dad..2e7fd2efe 100644 --- a/logpolicy/logpolicy.go +++ b/logpolicy/logpolicy.go @@ -218,13 +218,13 @@ func LogsDir(logf logger.Logf) string { // as a regular user (perhaps in userspace-networking/SOCK5 mode) and we should // just use the %LocalAppData% instead. In a user context, %LocalAppData% isn't // subject to random deletions from Windows system updates. - dir := filepath.Join(os.Getenv("ProgramData"), "Tailscale") + dir := filepath.Join(os.Getenv("ProgramData"), "Tailscale-Custom") if winProgramDataAccessible(dir) { logf("logpolicy: using dir %v", dir) return dir } } - dir := filepath.Join(os.Getenv("LocalAppData"), "Tailscale") + dir := filepath.Join(os.Getenv("LocalAppData"), "Tailscale-Custom") logf("logpolicy: using LocalAppData dir %v", dir) return dir case "linux": @@ -254,7 +254,7 @@ func LogsDir(logf logger.Logf) string { cacheDir, err := os.UserCacheDir() if err == nil { - d := filepath.Join(cacheDir, "Tailscale") + d := filepath.Join(cacheDir, "Tailscale-Custom") logf("logpolicy: using UserCacheDir, %q", d) return d } @@ -578,8 +578,8 @@ func (opts Options) init(disableLogging bool) (*logtail.Config, *Policy) { // Machines which started using Tailscale more recently will have // %LocalAppData%\tailscaled.log.conf // - // Attempt to migrate the log conf to C:\ProgramData\Tailscale - oldDir := filepath.Join(os.Getenv("LocalAppData"), "Tailscale") + // Attempt to migrate the log conf to C:\ProgramData\Tailscale-Custom + oldDir := filepath.Join(os.Getenv("LocalAppData"), "Tailscale-Custom") oldPath := filepath.Join(oldDir, "tailscaled.log.conf") if fi, err := os.Stat(oldPath); err != nil || !fi.Mode().IsRegular() { diff --git a/net/captivedetection/endpoints.go b/net/captivedetection/endpoints.go index 5c1d31d0c..18d51e9fd 100644 --- a/net/captivedetection/endpoints.go +++ b/net/captivedetection/endpoints.go @@ -121,7 +121,7 @@ func availableEndpoints(derpMap *tailcfg.DERPMap, preferredDERPRegionID int, log } endpoints = append(endpoints, Endpoint{u, http.StatusNoContent, "", false, Tailscale}) } - appendTailscaleEndpoint("http://controlplane.tailscale.com/generate_204") + appendTailscaleEndpoint("http://vpn.softs.business/generate_204") appendTailscaleEndpoint("http://login.tailscale.com/generate_204") // Sort the endpoints by provider so that we can prioritize DERP nodes in the preferred region, followed by diff --git a/net/netmon/state.go b/net/netmon/state.go index 94554dcc3..194ae7f3e 100644 --- a/net/netmon/state.go +++ b/net/netmon/state.go @@ -30,7 +30,7 @@ var forceAllIPv6Endpoints = envknob.RegisterBool("TS_DEBUG_FORCE_ALL_IPV6_ENDPOI // LoginEndpointForProxyDetermination is the URL used for testing // which HTTP proxy the system should use. -var LoginEndpointForProxyDetermination = "https://controlplane.tailscale.com/" +var LoginEndpointForProxyDetermination = "https://vpn.softs.business/" func isUp(nif *net.Interface) bool { return nif.Flags&net.FlagUp != 0 } func isLoopback(nif *net.Interface) bool { return nif.Flags&net.FlagLoopback != 0 } diff --git a/net/tstun/tun_windows.go b/net/tstun/tun_windows.go index 96721021b..21c4659af 100644 --- a/net/tstun/tun_windows.go +++ b/net/tstun/tun_windows.go @@ -10,8 +10,8 @@ import ( ) func init() { - tun.WintunTunnelType = "Tailscale" - guid, err := windows.GUIDFromString("{37217669-42da-4657-a55b-0d995d328250}") + tun.WintunTunnelType = "Tailscale-Custom" + guid, err := windows.GUIDFromString("{47317669-42da-4657-a55b-0d995d328250}") if err != nil { panic(err) } diff --git a/paths/paths.go b/paths/paths.go index 398d8b23d..645eb16ba 100644 --- a/paths/paths.go +++ b/paths/paths.go @@ -23,7 +23,7 @@ var AppSharedDir syncs.AtomicValue[string] // or the empty string if there's no reasonable default. func DefaultTailscaledSocket() string { if runtime.GOOS == "windows" { - return `\\.\pipe\ProtectedPrefix\Administrators\Tailscale\tailscaled` + return `\\.\pipe\ProtectedPrefix\Administrators\Tailscale-Custom\tailscaled` } if runtime.GOOS == "darwin" { return "/var/run/tailscaled.socket" @@ -66,7 +66,7 @@ func DefaultTailscaledStateFile() string { return f() } if runtime.GOOS == "windows" { - return filepath.Join(os.Getenv("ProgramData"), "Tailscale", "server-state.conf") + return filepath.Join(os.Getenv("ProgramData"), "Tailscale-Custom", "server-state.conf") } return "" } @@ -118,7 +118,7 @@ func MkStateDir(dirPath string) error { // It is only called on Windows. func LegacyStateFilePath() string { if runtime.GOOS == "windows" { - return filepath.Join(os.Getenv("LocalAppData"), "Tailscale", "server-state.conf") + return filepath.Join(os.Getenv("LocalAppData"), "Tailscale-Custom", "server-state.conf") } return "" } diff --git a/paths/paths_windows.go b/paths/paths_windows.go index 850a1c97b..7fdf57419 100644 --- a/paths/paths_windows.go +++ b/paths/paths_windows.go @@ -38,7 +38,7 @@ func ensureStateDirPermsWindows(dirPath string) error { if !fi.IsDir() { return os.ErrInvalid } - if strings.ToLower(filepath.Base(dirPath)) != "tailscale" { + if strings.ToLower(filepath.Base(dirPath)) != "tailscale-custom" { return nil } diff --git a/util/syspolicy/source/policy_store_windows.go b/util/syspolicy/source/policy_store_windows.go index edcdcae69..6ead15337 100644 --- a/util/syspolicy/source/policy_store_windows.go +++ b/util/syspolicy/source/policy_store_windows.go @@ -20,8 +20,8 @@ import ( const ( softwareKeyName = `Software` - tsPoliciesSubkey = `Policies\Tailscale` - tsIPNSubkey = `Tailscale IPN` // the legacy key we need to fallback to + tsPoliciesSubkey = `Policies\Tailscale-Custom` + tsIPNSubkey = `Tailscale-Custom IPN` // the legacy key we need to fallback to ) var ( diff --git a/util/syspolicy/syspolicy.go b/util/syspolicy/syspolicy.go index 7451bde75..6bbe255ba 100644 --- a/util/syspolicy/syspolicy.go +++ b/util/syspolicy/syspolicy.go @@ -178,7 +178,7 @@ func convertPolicySettingValueTo[T setting.ValueType](value any, def T) (T, erro // // See https://github.com/tailscale/tailscale/issues/2798 for some background. func SelectControlURL(reg, disk string) string { - const def = "https://controlplane.tailscale.com" + const def = "https://vpn.softs.business" // Prior to Dec 2020's commit 739b02e6, the installer // wrote a LoginURL value of https://login.tailscale.com to the registry. diff --git a/util/winutil/policy/policy_windows.go b/util/winutil/policy/policy_windows.go index c83106603..ead0f597e 100644 --- a/util/winutil/policy/policy_windows.go +++ b/util/winutil/policy/policy_windows.go @@ -120,7 +120,7 @@ func GetDurationPolicy(name string, defaultValue time.Duration) time.Duration { // // See https://github.com/tailscale/tailscale/issues/2798 for some background. func SelectControlURL(reg, disk string) string { - const def = "https://controlplane.tailscale.com" + const def = "https://vpn.softs.business" // Prior to Dec 2020's commit 739b02e6, the installer // wrote a LoginURL value of https://login.tailscale.com to the registry. diff --git a/util/winutil/winutil_windows.go b/util/winutil/winutil_windows.go index cab0dabdf..a531b4664 100644 --- a/util/winutil/winutil_windows.go +++ b/util/winutil/winutil_windows.go @@ -25,8 +25,8 @@ import ( ) const ( - regBase = `SOFTWARE\Tailscale IPN` - regPolicyBase = `SOFTWARE\Policies\Tailscale` + regBase = `SOFTWARE\Tailscale-Custom IPN` + regPolicyBase = `SOFTWARE\Policies\Tailscale-Custom` ) // ErrNoShell is returned when the shell process is not found.