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)
This commit is contained in:
2026-04-10 17:16:09 +07:00
parent ec0b23a21f
commit 574c8ccdda
24 changed files with 870 additions and 35 deletions
+2 -2
View File
@@ -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)
+87
View File
@@ -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)
}
+586
View File
@@ -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
}
+2 -2
View File
@@ -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")
+1 -1
View File
@@ -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{
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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)
+1 -1
View File
@@ -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 {
+162
View File
@@ -0,0 +1,162 @@
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util">
<!-- Unique UpgradeCode for Tailscale-Custom (different from official Tailscale) -->
<Package Name="Tailscale-Custom"
Manufacturer="Tailscale-Custom"
Version="1.97.176.0"
UpgradeCode="E1F2A3B4-C5D6-4E7F-8A9B-0C1D2E3F4A5B"
Scope="perMachine"
Language="1033"
Compressed="yes">
<SummaryInformation Description="Tailscale-Custom VPN Client" />
<!-- Allow major upgrades, prevent downgrades -->
<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
<MediaTemplate EmbedCab="yes" />
<!-- Install directory structure -->
<StandardDirectory Id="ProgramFiles64Folder">
<Directory Id="INSTALLFOLDER" Name="Tailscale-Custom">
<!-- All files go here -->
</Directory>
</StandardDirectory>
<!-- Start Menu directory -->
<StandardDirectory Id="ProgramMenuFolder">
<Directory Id="TailscaleCustomMenuFolder" Name="Tailscale-Custom" />
</StandardDirectory>
<!-- Desktop directory -->
<StandardDirectory Id="DesktopFolder" />
<!-- Components -->
<ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
<!-- tailscaled.exe - the daemon/service -->
<Component Id="TailscaledExe" Guid="A1B2C3D4-E5F6-4A7B-8C9D-0E1F2A3B4C5D" Bitness="always64">
<File Id="tailscaled.exe"
Source="$(var.DistDir)\tailscaled.exe"
KeyPath="yes" />
<!-- Register as Windows Service -->
<ServiceInstall Id="TailscaleCustomService"
Name="Tailscale-Custom"
DisplayName="Tailscale-Custom"
Description="Connects this computer to others on the Tailscale-Custom network."
Type="ownProcess"
Start="auto"
ErrorControl="normal"
Account="LocalSystem">
<ServiceDependency Id="Dnscache" />
<ServiceDependency Id="iphlpsvc" />
<ServiceDependency Id="netprofm" />
<ServiceDependency Id="WinHttpAutoProxySvc" />
<util:ServiceConfig FirstFailureActionType="restart"
SecondFailureActionType="restart"
ThirdFailureActionType="restart"
RestartServiceDelayInSeconds="5"
ResetPeriodInDays="1" />
</ServiceInstall>
<!-- Control service lifecycle during install/uninstall -->
<ServiceControl Id="TailscaleCustomServiceControl"
Name="Tailscale-Custom"
Start="install"
Stop="both"
Remove="uninstall"
Wait="yes" />
</Component>
<!-- tailscale.exe - the CLI -->
<Component Id="TailscaleExe" Guid="B2C3D4E5-F6A7-4B8C-9D0E-1F2A3B4C5D6E" Bitness="always64">
<File Id="tailscale.exe"
Source="$(var.DistDir)\tailscale.exe"
KeyPath="yes" />
</Component>
<!-- wintun.dll - TUN driver required by tailscaled -->
<Component Id="WintunDll" Guid="A7B8C9D0-E1F2-4A3B-4C5D-6E7F8A9B0C1D" Bitness="always64">
<File Id="wintun.dll"
Source="$(var.DistDir)\wintun.dll"
KeyPath="yes" />
</Component>
<!-- Add install dir to system PATH -->
<Component Id="PathEnv" Guid="C3D4E5F6-A7B8-4C9D-0E1F-2A3B4C5D6E7F" Bitness="always64">
<Environment Id="PATH"
Name="PATH"
Value="[INSTALLFOLDER]"
Permanent="no"
Part="last"
Action="set"
System="yes" />
</Component>
<!-- tailscale-tray.exe - the system tray GUI -->
<Component Id="TailscaleTrayExe" Guid="D4E5F6A7-B8C9-4D0E-1F2A-3B4C5D6E7F8A" Bitness="always64">
<File Id="tailscale_tray.exe"
Source="$(var.DistDir)\tailscale-tray.exe"
KeyPath="yes" />
<!-- Auto-start tray app on login for all users -->
<RegistryValue Root="HKLM"
Key="SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
Name="Tailscale-Custom-Tray"
Type="string"
Value="&quot;[INSTALLFOLDER]tailscale-tray.exe&quot;" />
</Component>
</ComponentGroup>
<!-- Start Menu Shortcut -->
<ComponentGroup Id="StartMenuShortcuts" Directory="TailscaleCustomMenuFolder">
<Component Id="StartMenuShortcut" Guid="E5F6A7B8-C9D0-4E1F-2A3B-4C5D6E7F8A9B" Bitness="always64">
<Shortcut Id="TrayStartMenuShortcut"
Name="Tailscale-Custom"
Description="Tailscale-Custom VPN Client"
Target="[INSTALLFOLDER]tailscale-tray.exe"
WorkingDirectory="INSTALLFOLDER" />
<RemoveFolder Id="RemoveStartMenuFolder" On="uninstall" />
<RegistryValue Root="HKCU" Key="Software\Tailscale-Custom" Name="StartMenuInstalled" Type="integer" Value="1" KeyPath="yes" />
</Component>
</ComponentGroup>
<!-- Desktop Shortcut -->
<ComponentGroup Id="DesktopShortcuts" Directory="DesktopFolder">
<Component Id="DesktopShortcut" Guid="F6A7B8C9-D0E1-4F2A-3B4C-5D6E7F8A9B0C" Bitness="always64">
<Shortcut Id="TrayDesktopShortcut"
Name="Tailscale-Custom"
Description="Tailscale-Custom VPN Client"
Target="[INSTALLFOLDER]tailscale-tray.exe"
WorkingDirectory="INSTALLFOLDER" />
<RegistryValue Root="HKCU" Key="Software\Tailscale-Custom" Name="DesktopInstalled" Type="integer" Value="1" KeyPath="yes" />
</Component>
</ComponentGroup>
<!-- Feature definition -->
<Feature Id="ProductFeature" Title="Tailscale-Custom" Level="1">
<ComponentGroupRef Id="ProductComponents" />
<ComponentGroupRef Id="StartMenuShortcuts" />
<ComponentGroupRef Id="DesktopShortcuts" />
</Feature>
<!-- Launch tray app after install -->
<CustomAction Id="LaunchTray"
FileRef="tailscale_tray.exe"
ExeCommand=""
Impersonate="yes"
Return="asyncNoWait" />
<InstallExecuteSequence>
<Custom Action="LaunchTray" After="InstallFinalize" Condition="NOT Installed OR REINSTALL" />
</InstallExecuteSequence>
<!-- UI: minimal (progress bar only, no wizard dialogs) -->
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLFOLDER" />
</Package>
</Wix>
+1 -1
View File
@@ -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),
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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.
+1 -1
View File
@@ -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)
+5 -5
View File
@@ -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() {
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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 }
+2 -2
View File
@@ -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)
}
+3 -3
View File
@@ -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 ""
}
+1 -1
View File
@@ -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
}
@@ -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 (
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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.
+2 -2
View File
@@ -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.